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
6 changes: 6 additions & 0 deletions client/packages/lowcoder/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,17 @@ if (!apiServiceUrl && isDev) {
process.exit(1);
}

const proxyServiceUrl = process.env.LOWCODER_PROXY_SERVICE_URL || "http://localhost:6070";

const proxyConfig: ServerOptions["proxy"] = {
"/api": {
target: apiServiceUrl,
changeOrigin: false,
},
"/proxy": {
target: proxyServiceUrl,
changeOrigin: true,
},
};

if (nodeServiceUrl) {
Expand Down
1 change: 1 addition & 0 deletions deploy/docker/default-multi.env
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ LOWCODER_MONGODB_URL="mongodb://lowcoder:secret123@mongodb/lowcoder?authSource=a
LOWCODER_REDIS_URL="redis://redis:6379"
LOWCODER_NODE_SERVICE_URL="http://lowcoder-node-service:6060"
LOWCODER_API_SERVICE_URL="http://lowcoder-api-service:8080"
LOWCODER_PROXY_SERVICE_URL="http://lowcoder-proxy-service:6070"

15 changes: 15 additions & 0 deletions deploy/docker/default.env
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,21 @@ LOWCODER_API_RATE_LIMIT=100
LOWCODER_API_SERVICE_URL="http://localhost:8080"
# Lowcoder Node service URL
LOWCODER_NODE_SERVICE_URL="http://localhost:6060"
# Lowcoder Proxy service URL
# Local dev (proxy running on host): http://localhost:6070
# Docker multi setup: http://lowcoder-proxy-service:6070
LOWCODER_PROXY_SERVICE_URL="http://localhost:6070"
# Typeform proxy rate limit per minute per client IP
LOWCODER_PROXY_RATE_LIMIT=120
# Allowed upstream hosts for Typeform proxy
LOWCODER_PROXY_ALLOWED_HOSTS="form.typeform.com,embed.typeform.com,admin.typeform.com"
# Allowed upstream hosts for the Google Forms proxy (public forms)
LOWCODER_GOOGLE_FORMS_ALLOWED_HOSTS="docs.google.com"
# Hocuspocus URL injected into form iframe bridges for driver/follower sync
LOWCODER_HOCUSPOCUS_URL="ws://localhost:3006"
LOWCODER_HOCUSPOCUS_SECRET=""
# API service URL used by proxy-service to resolve logged-in user for session creation
LOWCODER_API_SERVICE_URL="http://localhost:8080"

#
# ! PLEASE CHANGE THESE TO SOMETHING UNIQUE !
Expand Down
31 changes: 31 additions & 0 deletions deploy/docker/docker-compose-multi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ services:
test: curl -sS http://lowcoder-node-service:6060 | grep -c "Lowcoder Node Service is up and running" > /dev/null
interval: 3s
timeout: 5s
retries: 15
start_period: 40s

lowcoder-proxy-service:
build:
context: ../../server/proxy-service
dockerfile: Dockerfile
container_name: lowcoder-proxy-service
ports:
- "6070:6070"
environment:
PROXY_SERVICE_PORT: "6070"
env_file:
- path: ./default.env
required: true
- path: ./default-multi.env
required: true
- path: ./override.env
required: false
restart: unless-stopped
depends_on:
lowcoder-api-service:
condition: service_healthy
restart: true
healthcheck:
test: curl -sS http://lowcoder-proxy-service:6070 | grep -c "Lowcoder Proxy Service is up and running" > /dev/null
interval: 3s
timeout: 5s
retries: 10

##
Expand All @@ -135,6 +163,9 @@ services:
lowcoder-api-service:
condition: service_healthy
restart: true
lowcoder-proxy-service:
condition: service_healthy
restart: true
volumes:
- ./lowcoder-stacks/assets:/lowcoder/assets
- ./lowcoder-stacks/ssl:/lowcoder-stacks/ssl
Expand Down
Empty file modified deploy/docker/frontend/01-update-nginx-conf.sh
100644 → 100755
Empty file.
12 changes: 12 additions & 0 deletions deploy/docker/frontend/server.conf
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,15 @@
proxy_pass __LOWCODER_NODE_SERVICE_URL__;
}

location /proxy/ {
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 120s;
proxy_pass __LOWCODER_PROXY_SERVICE_URL__;
}

5 changes: 5 additions & 0 deletions deploy/docker/override.env
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@
## ##
#####################################################################

# Proxy service runs in Docker (lowcoder-proxy-service container).
# Frontend nginx forwards /proxy/* to the proxy container on the internal network.
LOWCODER_PROXY_SERVICE_URL="http://lowcoder-proxy-service:6070"

# Public URL used when proxy-service builds proxied iframe links for the browser.
LOWCODER_PUBLIC_URL="http://localhost:3000/"
4 changes: 4 additions & 0 deletions server/proxy-service/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
build
yarn.lock
package-lock.json
15 changes: 15 additions & 0 deletions server/proxy-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
FROM node:20-alpine

WORKDIR /lowcoder/proxy-service

RUN apk add --no-cache curl

COPY package.json ./
RUN yarn install || npm install

COPY . .
RUN yarn build || npm run build

EXPOSE 6070

CMD ["node", "build/server.js"]
83 changes: 83 additions & 0 deletions server/proxy-service/build/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getSigningSecret = getSigningSecret;
exports.createProxyToken = createProxyToken;
exports.verifyProxyToken = verifyProxyToken;
exports.getBearerToken = getBearerToken;
exports.resolveParticipantId = resolveParticipantId;
exports.resolveEditorId = resolveEditorId;
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const node_crypto_1 = require("node:crypto");
const API_KEY_SECRET = process.env.LOWCODER_API_KEY_SECRET ?? "";
const TOKEN_TTL_MS = 60 * 60 * 1000;
const API_SERVICE_URL = (process.env.LOWCODER_API_SERVICE_URL ?? "http://localhost:8080").replace(/\/$/, "");
function getSigningSecret() {
if (!API_KEY_SECRET)
return null;
return Buffer.from(API_KEY_SECRET).toString("base64");
}
function createProxyToken(userId, roomId, role, scope = "typeform-proxy") {
const secret = getSigningSecret();
if (!secret) {
return jsonwebtoken_1.default.sign({ userId, roomId, role, scope }, "dev-proxy-secret", {
expiresIn: "1h",
});
}
return jsonwebtoken_1.default.sign({ sub: userId, userId, roomId, role, scope }, secret, {
algorithm: "HS256",
expiresIn: "1h",
});
}
function verifyProxyToken(token, expectedScope) {
if (!token)
return false;
const secret = getSigningSecret();
if (!secret)
return true;
try {
const payload = jsonwebtoken_1.default.verify(token, secret);
return !expectedScope || payload.scope === expectedScope;
}
catch {
return false;
}
}
function getBearerToken(authHeader) {
if (!authHeader || !authHeader.startsWith("Bearer "))
return null;
return authHeader.slice("Bearer ".length);
}
async function resolveParticipantId(req, options = {}) {
const editorId = options.editorId?.trim();
if (editorId)
return editorId;
const guestId = options.guestId?.trim();
if (guestId)
return guestId;
const cookie = req.headers.cookie;
if (cookie) {
const response = await (0, node_fetch_1.default)(`${API_SERVICE_URL}/api/users/me`, {
headers: { cookie },
});
if (response.ok) {
const payload = (await response.json());
const userId = payload?.data?.id?.trim();
if (userId)
return userId;
}
}
const roomId = options.roomId?.trim();
const role = (options.role?.trim() || "driver").trim() || "driver";
if (roomId) {
return `guest-${roomId}-${role}`;
}
return `guest-${(0, node_crypto_1.randomUUID)()}`;
}
/** @deprecated Use resolveParticipantId */
async function resolveEditorId(req, fallbackEditorId) {
return resolveParticipantId(req, { editorId: fallbackEditorId });
}
Loading
Loading