# @flink-app/generic-request-plugin

A lightweight Flink plugin that allows you to register custom request handlers with direct access to Express request and response objects. This plugin is useful when you need to bypass Flink's standard handler system and implement custom request handling logic.

## Features

- Register custom Express route handlers
- Direct access to Express `req`, `res`, and Flink `app` objects
- Support for all HTTP methods (GET, POST, PUT, DELETE)
- Optional authentication and permission validation
- Bypass Flink's standard validation and response handling
- Useful for webhooks, custom integrations, and special endpoints

## Installation

```bash
npm install @flink-app/generic-request-plugin
```

## Usage

### Basic Setup

```typescript
import { FlinkApp } from "@flink-app/flink";
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";

function start() {
  new FlinkApp<AppContext>({
    name: "My app",
    plugins: [
      genericRequestPlugin({
        path: "/custom-endpoint",
        method: HttpMethod.get,
        handler: (req, res, app) => {
          res.setHeader("Content-Type", "text/html");
          res.end("Hello world!");
        },
      }),
    ],
  }).start();
}
```

## Configuration Options

- `path` (required): The URL path for the endpoint (e.g., `/webhook`, `/custom`)
- `method` (required): HTTP method for the request
- `handler` (required): Function that handles the request
- `permissions` (optional): Permission(s) required to access this route (string or array of strings)

## HTTP Methods

The plugin supports the following HTTP methods via the `HttpMethod` enum:

- `HttpMethod.get` - GET requests
- `HttpMethod.post` - POST requests
- `HttpMethod.put` - PUT requests
- `HttpMethod.delete` - DELETE requests

## Handler Function

The handler function receives three parameters:

```typescript
handler: (req, res, app) => void
```

- `req` - Express Request object
- `res` - Express Response object
- `app` - FlinkApp instance (provides access to context, repos, etc.)

## Examples

### Simple GET Endpoint

```typescript
genericRequestPlugin({
  path: "/hello",
  method: HttpMethod.get,
  handler: (req, res, app) => {
    res.json({ message: "Hello from Flink!" });
  },
});
```

### POST Endpoint with JSON Body

```typescript
genericRequestPlugin({
  path: "/webhook",
  method: HttpMethod.post,
  handler: (req, res, app) => {
    const payload = req.body;
    console.log("Received webhook:", payload);
    res.status(200).json({ received: true });
  },
});
```

### Endpoint with Database Access

```typescript
genericRequestPlugin({
  path: "/custom-data",
  method: HttpMethod.get,
  handler: async (req, res, app) => {
    try {
      const data = await app.ctx.repos.myRepo.findAll();
      res.json({ data });
    } catch (error) {
      res.status(500).json({ error: "Failed to fetch data" });
    }
  },
});
```

### Endpoint with Query Parameters

```typescript
genericRequestPlugin({
  path: "/search",
  method: HttpMethod.get,
  handler: (req, res, app) => {
    const query = req.query.q;
    const results = performSearch(query);
    res.json({ results });
  },
});
```

### Endpoint with Path Parameters

```typescript
genericRequestPlugin({
  path: "/user/:id",
  method: HttpMethod.get,
  handler: async (req, res, app) => {
    const userId = req.params.id;
    const user = await app.ctx.repos.userRepo.findById(userId);

    if (!user) {
      res.status(404).json({ error: "User not found" });
      return;
    }

    res.json({ user });
  },
});
```

### Custom HTML Response

```typescript
genericRequestPlugin({
  path: "/status",
  method: HttpMethod.get,
  handler: (req, res, app) => {
    const html = `
      <!DOCTYPE html>
      <html>
        <head><title>Status</title></head>
        <body>
          <h1>System Status: OK</h1>
          <p>All systems operational</p>
        </body>
      </html>
    `;
    res.setHeader("Content-Type", "text/html");
    res.end(html);
  },
});
```

### Webhook Handler

```typescript
genericRequestPlugin({
  path: "/webhook/stripe",
  method: HttpMethod.post,
  handler: async (req, res, app) => {
    const signature = req.headers["stripe-signature"];
    const payload = req.body;

    // Verify webhook signature
    if (!verifyStripeSignature(payload, signature)) {
      res.status(401).json({ error: "Invalid signature" });
      return;
    }

    // Process webhook
    await app.ctx.repos.orderRepo.updatePaymentStatus(payload.data);

    res.status(200).json({ received: true });
  },
});
```

### File Download Endpoint

```typescript
genericRequestPlugin({
  path: "/download/:fileId",
  method: HttpMethod.get,
  handler: async (req, res, app) => {
    const fileId = req.params.fileId;
    const file = await app.ctx.repos.fileRepo.findById(fileId);

    if (!file) {
      res.status(404).json({ error: "File not found" });
      return;
    }

    res.setHeader("Content-Type", file.mimeType);
    res.setHeader("Content-Disposition", `attachment; filename="${file.name}"`);
    res.end(file.buffer);
  },
});
```

### Custom Manual Authentication

If you need full control over authentication, you can implement it manually in your handler:

```typescript
genericRequestPlugin({
  path: "/admin/action",
  method: HttpMethod.post,
  handler: async (req, res, app) => {
    const apiKey = req.headers["x-api-key"];

    // Custom authentication logic
    if (apiKey !== process.env.ADMIN_API_KEY) {
      res.status(401).json({ error: "Unauthorized" });
      return;
    }

    // Perform admin action
    const result = await performAdminAction(req.body);
    res.json({ success: true, result });
  },
});
```

## Authentication and Permissions

Starting with version 0.12.1-alpha.46, the generic request plugin supports automatic authentication and permission validation, just like standard Flink handlers.

### Prerequisites

You must configure an auth plugin (such as `@flink-app/jwt-auth-plugin`) in your FlinkApp:

```typescript
import { FlinkApp } from "@flink-app/flink";
import { jwtAuthPlugin } from "@flink-app/jwt-auth-plugin";
import { genericRequestPlugin, HttpMethod } from "@flink-app/generic-request-plugin";

const app = new FlinkApp<AppContext>({
  name: "My app",
  auth: jwtAuthPlugin({
    secret: process.env.JWT_SECRET!,
    getUser: async (tokenData) => {
      const user = await app.ctx.repos.userRepo.findById(tokenData.userId);
      return {
        id: user._id,
        username: user.username,
        roles: user.roles,
      };
    },
    rolePermissions: {
      admin: ["read", "write", "delete", "admin:webhooks"],
      user: ["read", "write"],
    },
  }),
  plugins: [
    // Your generic request plugins here
  ],
});
```

### Basic Authentication

Use the `permissions` option to require authentication:

```typescript
genericRequestPlugin({
  path: "/webhook/admin",
  method: HttpMethod.post,
  permissions: "*", // Require any authenticated user
  handler: (req, res, app) => {
    // req.user is automatically populated with authenticated user
    console.log(`Webhook called by: ${req.user.username}`);
    res.json({ success: true });
  },
});
```

### Single Permission

Require a specific permission:

```typescript
genericRequestPlugin({
  path: "/admin/action",
  method: HttpMethod.post,
  permissions: "admin:webhooks", // User must have this permission
  handler: (req, res, app) => {
    // Only users with "admin:webhooks" permission can access
    const result = performAdminAction(req.body);
    res.json({ result });
  },
});
```

### Multiple Permissions

Require multiple permissions (user must have ALL):

```typescript
genericRequestPlugin({
  path: "/sensitive/data",
  method: HttpMethod.get,
  permissions: ["read", "admin:sensitive"], // User needs BOTH permissions
  handler: (req, res, app) => {
    const data = app.ctx.repos.sensitiveRepo.findAll();
    res.json({ data });
  },
});
```

### Accessing Authenticated User

When permissions are validated, the authenticated user is available via `req.user`:

```typescript
genericRequestPlugin({
  path: "/webhook/process",
  method: HttpMethod.post,
  permissions: "webhook:process",
  handler: async (req, res, app) => {
    // Access authenticated user properties
    const userId = req.user.id;
    const username = req.user.username;
    const roles = req.user.roles;

    // Use user info in your logic
    await app.ctx.repos.auditRepo.create({
      userId,
      action: "webhook_processed",
      payload: req.body,
    });

    res.json({ success: true });
  },
});
```

### How It Works

The permission validation works identically to standard Flink handlers:

1. **Token extraction**: The auth plugin extracts the JWT token (default: `Authorization: Bearer <token>` header)
2. **Token validation**: Token is decoded and validated
3. **Permission check**: User's roles are checked against required permissions
4. **User attachment**: If successful, user is attached to `req.user`
5. **Handler execution**: Your handler is only called if authentication succeeds
6. **Auto 401**: If authentication fails, a 401 response is returned automatically

### Advanced: Custom Token Extraction

The auth plugin's `tokenExtractor` option works with generic request handlers:

```typescript
// In your FlinkApp setup
auth: jwtAuthPlugin({
  secret: process.env.JWT_SECRET!,
  getUser: async (tokenData) => { /* ... */ },
  rolePermissions: { /* ... */ },
  tokenExtractor: (req) => {
    // Allow query param tokens for webhook routes
    if (req.path?.startsWith('/webhook/')) {
      return req.query?.token as string || null;
    }
    return undefined; // Use default Bearer token
  },
}),

// Your generic request handler
genericRequestPlugin({
  path: "/webhook/external",
  method: HttpMethod.post,
  permissions: "webhook:external",
  handler: (req, res, app) => {
    // Token can come from query param for this route
    res.json({ success: true });
  },
});
```

### Advanced: Dynamic Permissions

The auth plugin's `checkPermissions` callback works with generic request handlers:

```typescript
// In your FlinkApp setup
auth: jwtAuthPlugin({
  secret: process.env.JWT_SECRET!,
  getUser: async (tokenData) => {
    const user = await ctx.repos.userRepo.getById(tokenData.userId);
    const permissions = await ctx.repos.permissionRepo.getUserPermissions(user._id);
    return {
      id: user._id,
      username: user.username,
      permissions, // Attach DB permissions
    };
  },
  rolePermissions: {},
  checkPermissions: async (user, routePermissions) => {
    // Custom permission logic from database
    return routePermissions.every(perm => user.permissions?.includes(perm));
  },
}),

// Your generic request handler
genericRequestPlugin({
  path: "/custom/action",
  method: HttpMethod.post,
  permissions: ["custom:action", "premium:feature"],
  handler: (req, res, app) => {
    // Permissions validated against database
    res.json({ success: true });
  },
});
```

### Authentication Error Response

If authentication fails, the plugin automatically returns:

```json
{
  "status": 401,
  "error": {
    "title": "Unauthorized",
    "detail": "Authentication required or insufficient permissions"
  }
}
```

### Without Permissions

If you don't specify `permissions`, the route is public (no authentication required):

```typescript
genericRequestPlugin({
  path: "/public/webhook",
  method: HttpMethod.post,
  // No permissions = public route
  handler: (req, res, app) => {
    // Anyone can access this
    res.json({ received: true });
  },
});
```

## Multiple Endpoints

You can register multiple generic request handlers by including multiple plugin instances:

```typescript
new FlinkApp<AppContext>({
  name: "My app",
  plugins: [
    genericRequestPlugin({
      path: "/webhook/stripe",
      method: HttpMethod.post,
      handler: stripeWebhookHandler,
    }),
    genericRequestPlugin({
      path: "/webhook/github",
      method: HttpMethod.post,
      handler: githubWebhookHandler,
    }),
    genericRequestPlugin({
      path: "/status",
      method: HttpMethod.get,
      handler: statusHandler,
    }),
  ],
}).start();
```

## When to Use This Plugin

Use the generic request plugin when you need to:

- Handle webhooks from external services
- Return non-JSON responses (HTML, XML, plain text, binary files)
- Bypass Flink's request validation
- Access low-level Express request/response objects
- Implement server-sent events (SSE)
- Handle file uploads/downloads with custom logic
- Integrate with third-party services requiring specific response formats
- Require authentication but need custom response handling

## When NOT to Use This Plugin

For standard API endpoints, use Flink's built-in handler system instead:

- Standard CRUD operations - Use `FlinkHttpHandler` with GET/POST/PUT/DELETE
- Endpoints requiring validation - Use Flink's schema validation
- Type-safe endpoints - Use Flink's typed handlers
- Auto-documented endpoints - Use standard handlers with the API docs plugin

## Access to FlinkApp Context

The handler receives the full FlinkApp instance, giving you access to:

```typescript
handler: (req, res, app) => {
  // Access repositories
  const data = await app.ctx.repos.myRepo.findAll();

  // Access plugins
  const stripeAPI = app.ctx.plugins.stripePlugin.stripeAPI;

  // Access database
  const collection = app.db.collection("myCollection");

  // Access any custom context properties
  const config = app.ctx.config;
}
```

## Error Handling

Always implement proper error handling in your custom handlers:

```typescript
genericRequestPlugin({
  path: "/custom",
  method: HttpMethod.post,
  handler: async (req, res, app) => {
    try {
      // Your logic here
      const result = await performOperation(req.body);
      res.json({ success: true, result });
    } catch (error) {
      console.error("Error in custom handler:", error);
      res.status(500).json({
        error: "Internal server error",
        message: error.message
      });
    }
  },
});
```

## TypeScript Support

The plugin includes TypeScript definitions. For better type safety, you can type your handlers:

```typescript
import { Request, Response } from "express";
import { FlinkApp } from "@flink-app/flink";

const myHandler = async (
  req: Request,
  res: Response,
  app: FlinkApp<AppContext>
) => {
  // Fully typed handler
  const userId: string = req.params.id;
  const user = await app.ctx.repos.userRepo.findById(userId);
  res.json({ user });
};

genericRequestPlugin({
  path: "/user/:id",
  method: HttpMethod.get,
  handler: myHandler,
});
```

## Security Considerations

- Always validate and sanitize input data
- Use the `permissions` option for routes requiring authentication
- For manual authentication, implement it carefully and consistently
- Be careful with direct database access
- Validate webhook signatures for external integrations
- Use HTTPS in production
- Rate limit sensitive endpoints
- Never expose sensitive information in error messages
- Remember: without `permissions` set, your route is publicly accessible

## License

MIT
