Skip to content
Open
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
26 changes: 26 additions & 0 deletions docs/guide/standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,22 @@ You can change the default CORS settings for redeeming tokens, generating challe
You can change the default port and hostname by setting the `SERVER_PORT` and `SERVER_HOSTNAME` environment variables when running the server. This defaults are `0.0.0.0` for hostname and `3000` for port.
If you change port or hostname, you need to adapt Docker's port forwarding.

### Custom Base Path

You can provide a custom root URL for UI and API access by setting the `BASE_PATH` environment variables when running the server. This defaults to no base path, serving all requests from the hostname root. The BASE_PATH is automatically normalized to start with `/` and to not end with `/`.

Example running server with custom root URL:

```bash
docker run -d \
-p 3000:3000 \
-v cap-data:/usr/src/app/.data \
-e ADMIN_KEY=your_secret_key \
-e BASE_PATH=/cap \
--name cap-standalone \
tiago2/cap:latest
```

## Usage

### Client-side
Expand All @@ -67,6 +83,16 @@ Example:
></cap-widget>
```

When using the Cap widget with a custom BASE_PATH, you must include the base path in the `data-cap-api-endpoint` attribute:

```html
<!-- For BASE_PATH=/cap -->
<cap-widget
data-cap-api-endpoint="https://cap.example.com/cap/d9256640cb53/api/"
data-cap-hidden-field-name="cap-token">
</cap-widget>
```

> [!TIP]
> Does generating challenges not work? Make sure your server is publicly accessible and that `CORS_ORIGIN` is set correctly to allow requests from your website's origin.

Expand Down
5 changes: 4 additions & 1 deletion standalone/public/js/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ export default async (method, path, body) => {
requestInit.body = JSON.stringify(body);
}

const json = await (await fetch(`/server${path}`, requestInit)).json();
// Extract base path from current URL
const currentPath = window.location.pathname;
const basePath = currentPath === '/' ? '' : currentPath.replace(/\/$/, '');
const json = await (await fetch(`${basePath}/server${path}`, requestInit)).json();

if (json?.error === "Unauthorized") {
localStorage.removeItem("cap_auth");
Expand Down
6 changes: 5 additions & 1 deletion standalone/public/login.html
Original file line number Diff line number Diff line change
Expand Up @@ -251,8 +251,12 @@ <h1>Welcome to Cap!</h1>

document.querySelector("button[type=submit]").disabled = true;

// Extract base path from current URL
const currentPath = window.location.pathname;
const basePath = currentPath === '/' ? '' : currentPath.replace(/\/$/, '');

const { success, session_token, hashed_token, expires } = await (
await fetch("/auth/login", {
await fetch(`${basePath}/auth/login`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Expand Down
9 changes: 8 additions & 1 deletion standalone/src/assets.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,15 @@ const updateCache = async () => {
updateCache();
setInterval(updateCache, 1000 * 60 * 60);

// Normalize BASE_PATH
let basePath = process.env.BASE_PATH || '';
if (basePath) {
if (!basePath.startsWith('/')) basePath = '/' + basePath;
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
}

export const assetsServer = new Elysia({
prefix: "/assets",
prefix: `${basePath}/assets`,
detail: { tags: ["Assets"] },
})
.use(
Expand Down
9 changes: 8 additions & 1 deletion standalone/src/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ if (ADMIN_KEY.length < 30)
"auth: Admin key too short. Please use one that's at least 30 characters",
);

// Normalize BASE_PATH
let basePath = process.env.BASE_PATH || '';
if (basePath) {
if (!basePath.startsWith('/')) basePath = '/' + basePath;
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
}

export const auth = new Elysia({
prefix: "/auth",
prefix: basePath + "/auth",
})
.use(
rateLimit({
Expand Down
8 changes: 8 additions & 0 deletions standalone/src/cap.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@ const upsertSolutionQuery = db.query(`
DO UPDATE SET count = count + 1
`);

// Normalize BASE_PATH
let basePath = process.env.BASE_PATH || '';
if (basePath) {
if (!basePath.startsWith('/')) basePath = '/' + basePath;
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
}

export const capServer = new Elysia({
prefix: basePath,
detail: {
tags: ["Challenges"],
},
Expand Down
26 changes: 22 additions & 4 deletions standalone/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@ import { server } from "./server.js";
const serverPort = process.env.SERVER_PORT || 3000;
const serverHostname = process.env.SERVER_HOSTNAME || '0.0.0.0'

// Normalize BASE_PATH: optional, defaults to no base path, must start with '/' and not end with '/'
let basePath = process.env.BASE_PATH || '';
if (basePath) {
if (!basePath.startsWith('/')) basePath = '/' + basePath;
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
}

new Elysia({
serve: {
port: serverPort,
Expand All @@ -20,7 +27,7 @@ new Elysia({
scalarConfig: {
customCss: `.section-header-wrapper .section-header.tight { margin-top: 10px; }`,
},
exclude: ["/", "/auth/login"],
exclude: [basePath + "/", basePath + "/auth/login"],
documentation: {
tags: [
{
Expand Down Expand Up @@ -56,8 +63,19 @@ new Elysia({
},
}),
)
.use(staticPlugin())
.get("/", async ({ cookie }) => {
.use(staticPlugin({
assets: "./public",
prefix: `${basePath}/public`
}))
.get(basePath, ({ set }) => {
// Redirect to add trailing slash when base path is configured
if (basePath) {
set.status = 301;
set.headers.Location = basePath + "/";
return "Redirecting...";
}
})
.get(basePath + "/", async ({ cookie }) => {
return file(
cookie.cap_authed?.value === "yes"
? "./public/index.html"
Expand All @@ -70,4 +88,4 @@ new Elysia({
.use(capServer)
.listen(serverPort);

console.log(`🧢 Cap running on http://${serverHostname}:${serverPort}`);
console.log(`🧢 Cap running on http://${serverHostname}:${serverPort}${basePath}`);
9 changes: 8 additions & 1 deletion standalone/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,15 @@ const get24hPreviousQuery = db.query(`
`);
const getKeysQuery = db.query(`SELECT * FROM keys ORDER BY created DESC`);

// Normalize BASE_PATH
let basePath = process.env.BASE_PATH || '';
if (basePath) {
if (!basePath.startsWith('/')) basePath = '/' + basePath;
if (basePath.endsWith('/')) basePath = basePath.slice(0, -1);
}

export const server = new Elysia({
prefix: "/server",
prefix: basePath + "/server",
detail: {
security: [
{
Expand Down