Compare commits
No commits in common. "5fcf76066f5d95194ac111e9b7ba074e1ef9380f" and "ba33064852760eac731922567f000d0fb4b7bce9" have entirely different histories.
5fcf76066f
...
ba33064852
|
@ -26,6 +26,3 @@ install/
|
||||||
bruno/
|
bruno/
|
||||||
LICENSE
|
LICENSE
|
||||||
CONTRIBUTING.md
|
CONTRIBUTING.md
|
||||||
dist
|
|
||||||
.git
|
|
||||||
config/
|
|
27
.github/dependabot.yml
vendored
|
@ -33,30 +33,3 @@ updates:
|
||||||
minor-updates:
|
minor-updates:
|
||||||
update-types:
|
update-types:
|
||||||
- "minor"
|
- "minor"
|
||||||
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
|
|
||||||
- package-ecosystem: "gomod"
|
|
||||||
directory: "/install"
|
|
||||||
schedule:
|
|
||||||
interval: "daily"
|
|
||||||
groups:
|
|
||||||
dev-patch-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
dev-minor-updates:
|
|
||||||
dependency-type: "development"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
||||||
prod-patch-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "patch"
|
|
||||||
prod-minor-updates:
|
|
||||||
dependency-type: "production"
|
|
||||||
update-types:
|
|
||||||
- "minor"
|
|
10
.github/workflows/cicd.yml
vendored
|
@ -12,13 +12,13 @@ jobs:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v2
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
||||||
|
@ -28,9 +28,9 @@ jobs:
|
||||||
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v4
|
||||||
with:
|
with:
|
||||||
go-version: 1.24
|
go-version: 1.23.0
|
||||||
|
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: |
|
run: |
|
||||||
|
|
34
.github/workflows/linting.yml
vendored
|
@ -1,34 +0,0 @@
|
||||||
name: ESLint
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- '**/*.js'
|
|
||||||
- '**/*.jsx'
|
|
||||||
- '**/*.ts'
|
|
||||||
- '**/*.tsx'
|
|
||||||
- '.eslintrc*'
|
|
||||||
- 'package.json'
|
|
||||||
- 'yarn.lock'
|
|
||||||
- 'pnpm-lock.yaml'
|
|
||||||
- 'package-lock.json'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
Linter:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v5
|
|
||||||
|
|
||||||
- name: Set up Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
npm ci
|
|
||||||
|
|
||||||
- name: Run ESLint
|
|
||||||
run: |
|
|
||||||
npx eslint . --ext .js,.jsx,.ts,.tsx
|
|
14
.github/workflows/test.yml
vendored
|
@ -11,11 +11,11 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '20'
|
||||||
|
|
||||||
- name: Copy config file
|
- name: Copy config file
|
||||||
run: cp config/config.example.yml config/config.yml
|
run: cp config/config.example.yml config/config.yml
|
||||||
|
@ -23,9 +23,6 @@ jobs:
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Create database index.ts
|
|
||||||
run: echo 'export * from "./sqlite";' > server/db/index.ts
|
|
||||||
|
|
||||||
- name: Generate database migrations
|
- name: Generate database migrations
|
||||||
run: npm run db:sqlite:generate
|
run: npm run db:sqlite:generate
|
||||||
|
|
||||||
|
@ -48,8 +45,5 @@ jobs:
|
||||||
echo "App failed to start"
|
echo "App failed to start"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image
|
||||||
run: make build-sqlite
|
run: make build
|
||||||
|
|
||||||
- name: Build Docker image pg
|
|
||||||
run: make build-pg
|
|
||||||
|
|
11
.gitignore
vendored
|
@ -18,7 +18,6 @@ yarn-error.log*
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
!Dockerfile.sqlite
|
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
*.log
|
*.log
|
||||||
.machinelogs*.json
|
.machinelogs*.json
|
||||||
|
@ -26,10 +25,6 @@ next-env.d.ts
|
||||||
migrations
|
migrations
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
config/config.yml
|
config/config.yml
|
||||||
config/postgres
|
|
||||||
config/postgres*
|
|
||||||
config/openapi.yaml
|
|
||||||
config/key
|
|
||||||
dist
|
dist
|
||||||
.dist
|
.dist
|
||||||
installer
|
installer
|
||||||
|
@ -38,9 +33,3 @@ bin
|
||||||
.secrets
|
.secrets
|
||||||
test_event.json
|
test_event.json
|
||||||
.idea/
|
.idea/
|
||||||
public/branding
|
|
||||||
server/db/index.ts
|
|
||||||
server/build.ts
|
|
||||||
postgres/
|
|
||||||
dynamic/
|
|
||||||
certificates/
|
|
||||||
|
|
2
.nvmrc
|
@ -1 +1 @@
|
||||||
22
|
20
|
||||||
|
|
|
@ -4,7 +4,11 @@ Contributions are welcome!
|
||||||
|
|
||||||
Please see the contribution and local development guide on the docs page before getting started:
|
Please see the contribution and local development guide on the docs page before getting started:
|
||||||
|
|
||||||
https://docs.digpangolin.com/development/contributing
|
https://docs.fossorial.io/development
|
||||||
|
|
||||||
|
For ideas about what features to work on and our future plans, please see the roadmap:
|
||||||
|
|
||||||
|
https://docs.fossorial.io/roadmap
|
||||||
|
|
||||||
### Licensing Considerations
|
### Licensing Considerations
|
||||||
|
|
||||||
|
|
21
Dockerfile
|
@ -1,26 +1,20 @@
|
||||||
FROM node:22-alpine AS builder
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ARG BUILD=oss
|
|
||||||
ARG DATABASE=sqlite
|
|
||||||
|
|
||||||
# COPY package.json package-lock.json ./
|
# COPY package.json package-lock.json ./
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
RUN echo "export * from \"./$DATABASE\";" > server/db/index.ts
|
RUN echo 'export * from "./sqlite";' > server/db/index.ts
|
||||||
|
|
||||||
RUN echo "export const build = \"$BUILD\" as any;" > server/build.ts
|
RUN npx drizzle-kit generate --dialect sqlite --schema ./server/db/sqlite/schema.ts --out init
|
||||||
|
|
||||||
RUN if [ "$DATABASE" = "pg" ]; then npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init; else npx drizzle-kit generate --dialect $DATABASE --schema ./server/db/$DATABASE/schema.ts --out init; fi
|
RUN npm run build:sqlite
|
||||||
|
|
||||||
RUN npm run build:$DATABASE
|
FROM node:20-alpine AS runner
|
||||||
RUN npm run build:cli
|
|
||||||
|
|
||||||
FROM node:22-alpine AS runner
|
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
@ -36,11 +30,8 @@ COPY --from=builder /app/.next/static ./.next/static
|
||||||
COPY --from=builder /app/dist ./dist
|
COPY --from=builder /app/dist ./dist
|
||||||
COPY --from=builder /app/init ./dist/init
|
COPY --from=builder /app/init ./dist/init
|
||||||
|
|
||||||
COPY ./cli/wrapper.sh /usr/local/bin/pangctl
|
|
||||||
RUN chmod +x /usr/local/bin/pangctl ./dist/cli.mjs
|
|
||||||
|
|
||||||
COPY server/db/names.json ./dist/names.json
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
|
||||||
COPY public ./public
|
COPY public ./public
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start:sqlite"]
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
FROM node:22-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
RUN npm ci
|
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
|
||||||
|
|
||||||
# Use tsx watch for development with hot reload
|
|
||||||
CMD ["npm", "run", "dev"]
|
|
37
Dockerfile.pg
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
FROM node:20-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# COPY package.json package-lock.json ./
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
RUN echo 'export * from "./pg";' > server/db/index.ts
|
||||||
|
|
||||||
|
RUN npx drizzle-kit generate --dialect postgresql --schema ./server/db/pg/schema.ts --out init
|
||||||
|
|
||||||
|
RUN npm run build:pg
|
||||||
|
|
||||||
|
FROM node:20-alpine AS runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Curl used for the health checks
|
||||||
|
RUN apk add --no-cache curl
|
||||||
|
|
||||||
|
# COPY package.json package-lock.json ./
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm ci --omit=dev && npm cache clean --force
|
||||||
|
|
||||||
|
COPY --from=builder /app/.next/standalone ./
|
||||||
|
COPY --from=builder /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder /app/dist ./dist
|
||||||
|
COPY --from=builder /app/init ./dist/init
|
||||||
|
|
||||||
|
COPY server/db/names.json ./dist/names.json
|
||||||
|
|
||||||
|
COPY public ./public
|
||||||
|
|
||||||
|
CMD ["npm", "run", "start:pg"]
|
17
Makefile
|
@ -1,14 +1,14 @@
|
||||||
.PHONY: build build-pg build-release build-arm build-x86 test clean
|
.PHONY: build build-release build-arm build-x86 test clean
|
||||||
|
|
||||||
build-release:
|
build-release:
|
||||||
@if [ -z "$(tag)" ]; then \
|
@if [ -z "$(tag)" ]; then \
|
||||||
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
echo "Error: tag is required. Usage: make build-release tag=<tag>"; \
|
||||||
exit 1; \
|
exit 1; \
|
||||||
fi
|
fi
|
||||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:latest -f Dockerfile --push .
|
||||||
docker buildx build --build-arg DATABASE=sqlite --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:$(tag) -f Dockerfile --push .
|
||||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-latest -f Dockerfile.pg --push .
|
||||||
docker buildx build --build-arg DATABASE=pg --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) --push .
|
docker buildx build --platform linux/arm64,linux/amd64 -t fosrl/pangolin:postgresql-$(tag) -f Dockerfile.pg --push .
|
||||||
|
|
||||||
build-arm:
|
build-arm:
|
||||||
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/arm64 -t fosrl/pangolin:latest .
|
||||||
|
@ -16,11 +16,8 @@ build-arm:
|
||||||
build-x86:
|
build-x86:
|
||||||
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
docker buildx build --platform linux/amd64 -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-sqlite:
|
build:
|
||||||
docker build --build-arg DATABASE=sqlite -t fosrl/pangolin:latest .
|
docker build -t fosrl/pangolin:latest .
|
||||||
|
|
||||||
build-pg:
|
|
||||||
docker build --build-arg DATABASE=pg -t fosrl/pangolin:postgresql-latest .
|
|
||||||
|
|
||||||
test:
|
test:
|
||||||
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
docker run -it -p 3000:3000 -p 3001:3001 -p 3002:3002 -v ./config:/app/config fosrl/pangolin:latest
|
||||||
|
|
107
README.md
|
@ -7,20 +7,20 @@
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 align="center">Secure gateway to your private networks</h4>
|
<h4 align="center">Tunneled Reverse Proxy Server with Access Control</h4>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
_Pangolin tunnels your services to the internet so you can access anything from anywhere._
|
_Your own self-hosted zero trust tunnel._
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<h5>
|
<h5>
|
||||||
<a href="https://digpangolin.com">
|
<a href="https://fossorial.io">
|
||||||
Website
|
Website
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
<a href="https://docs.digpangolin.com/self-host/quick-install">
|
<a href="https://docs.fossorial.io/Getting%20Started/quick-install">
|
||||||
Install Guide
|
Install Guide
|
||||||
</a>
|
</a>
|
||||||
<span> | </span>
|
<span> | </span>
|
||||||
|
@ -36,31 +36,22 @@ _Pangolin tunnels your services to the internet so you can access anything from
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<strong>
|
|
||||||
Start testing Pangolin at <a href="https://pangolin.fossorial.io/auth/signup">pangolin.fossorial.io</a>
|
|
||||||
</strong>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
|
Pangolin is a self-hosted tunneled reverse proxy server with identity and access control, designed to securely expose private resources on distributed networks. Acting as a central hub, it connects isolated networks — even those behind restrictive firewalls — through encrypted tunnels, enabling easy access to remote services without opening ports.
|
||||||
|
|
||||||
<img src="public/screenshots/hero.png" alt="Preview"/>
|
<img src="public/screenshots/hero.png" alt="Preview"/>
|
||||||
|
|
||||||

|
_Resources page of Pangolin dashboard (dark mode) showing multiple resources available to connect._
|
||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
### Reverse Proxy Through WireGuard Tunnel
|
### Reverse Proxy Through WireGuard Tunnel
|
||||||
|
|
||||||
- Expose private resources on your network **without opening ports** (firewall punching).
|
- Expose private resources on your network **without opening ports** (firewall punching).
|
||||||
- Secure and easy to configure private connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
- Secure and easy to configure site-to-site connectivity via a custom **user space WireGuard client**, [Newt](https://github.com/fosrl/newt).
|
||||||
- Built-in support for any WireGuard client.
|
- Built-in support for any WireGuard client.
|
||||||
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
- Automated **SSL certificates** (https) via [LetsEncrypt](https://letsencrypt.org/).
|
||||||
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
- Support for HTTP/HTTPS and **raw TCP/UDP services**.
|
||||||
- Load balancing.
|
- Load balancing.
|
||||||
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
|
||||||
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
|
||||||
- Attach as many sites to the central server as you wish.
|
|
||||||
|
|
||||||
### Identity & Access Management
|
### Identity & Access Management
|
||||||
|
|
||||||
|
@ -74,73 +65,89 @@ Pangolin is a self-hosted tunneled reverse proxy server with identity and access
|
||||||
- **Temporary, self-destructing share links.**
|
- **Temporary, self-destructing share links.**
|
||||||
- Resource specific pin codes.
|
- Resource specific pin codes.
|
||||||
- Resource specific passwords.
|
- Resource specific passwords.
|
||||||
- Passkeys
|
|
||||||
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
- External identity provider (IdP) support with OAuth2/OIDC, such as Authentik, Keycloak, Okta, and others.
|
||||||
- Auto-provision users and roles from your IdP.
|
- Auto-provision users and roles from your IdP.
|
||||||
|
|
||||||
<img src="public/auth-diagram1.png" alt="Auth and diagram"/>
|
### Simple Dashboard UI
|
||||||
|
|
||||||
## Use Cases
|
- Manage sites, users, and roles with a clean and intuitive UI.
|
||||||
|
- Monitor site usage and connectivity.
|
||||||
|
- Light and dark mode options.
|
||||||
|
- Mobile friendly.
|
||||||
|
|
||||||
### Manage Access to Internal Apps
|
### Easy Deployment
|
||||||
|
|
||||||
- Grant users access to your apps from anywhere using just a web browser. No client software required.
|
- Run on any cloud provider or on-premises.
|
||||||
|
- **Docker Compose based setup** for simplified deployment.
|
||||||
|
- Future-proof installation script for streamlined setup and feature additions.
|
||||||
|
- Use any WireGuard client to connect, or use **Newt, our custom user space client** for the best experience.
|
||||||
|
- Use the API to create custom integrations and scripts.
|
||||||
|
- Fine-grained access control to the API via scoped API keys.
|
||||||
|
- Comprehensive Swagger documentation for the API.
|
||||||
|
|
||||||
### Developers and DevOps
|
### Modular Design
|
||||||
|
|
||||||
- Expose and test internal tools and dashboards like **Grafana**. Bring localhost or private IPs online for easy access.
|
- Extend functionality with existing [Traefik](https://github.com/traefik/traefik) plugins, such as [CrowdSec](https://plugins.traefik.io/plugins/6335346ca4caa9ddeffda116/crowdsec-bouncer-traefik-plugin) and [Geoblock](https://github.com/PascalMinder/geoblock).
|
||||||
|
- **Automatically install and configure Crowdsec via Pangolin's installer script.**
|
||||||
|
- Attach as many sites to the central server as you wish.
|
||||||
|
|
||||||
### Secure API Gateway
|
<img src="public/screenshots/collage.png" alt="Collage"/>
|
||||||
|
|
||||||
- One application load balancer across multiple clouds and on-premises.
|
## Deployment and Usage Example
|
||||||
|
|
||||||
### IoT and Edge Devices
|
1. **Deploy the Central Server**:
|
||||||
|
|
||||||
- Easily expose **IoT devices**, **edge servers**, or **Raspberry Pi** to the internet for field equipment monitoring.
|
- Deploy the Docker Compose stack onto a VPS hosted on a cloud platform like RackNerd, Amazon EC2, DigitalOcean Droplet, or similar. There are many cheap VPS hosting options available to suit your needs.
|
||||||
|
|
||||||
<img src="public/screenshots/sites.png" alt="Sites"/>
|
|
||||||
|
|
||||||
## Deployment Options
|
|
||||||
|
|
||||||
### Fully Self Hosted
|
|
||||||
|
|
||||||
Host the full application on your own server or on the cloud with a VPS. Take a look at the [documentation](https://docs.digpangolin.com/self-host/quick-install) to get started.
|
|
||||||
|
|
||||||
|
> [!TIP]
|
||||||
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
> Many of our users have had a great experience with [RackNerd](https://my.racknerd.com/aff.php?aff=13788). Depending on promotions, you can get a [**VPS with 1 vCPU, 1GB RAM, and ~20GB SSD for just around $12/year**](https://my.racknerd.com/aff.php?aff=13788&pid=912). That's a great deal!
|
||||||
|
> We are part of the [RackNerd](https://my.racknerd.com/aff.php?aff=13788) affiliate program, so if you purchase through [our link](https://my.racknerd.com/aff.php?aff=13788), we receive a small commission which helps us maintain the project and keep it free for everyone.
|
||||||
|
|
||||||
### Pangolin Cloud
|
1. **Domain Configuration**:
|
||||||
|
|
||||||
Easy to use with simple [pay as you go pricing](https://digpangolin.com/pricing). [Check it out here](https://pangolin.fossorial.io/auth/signup).
|
- Point your domain name to the VPS and configure Pangolin with your preferred settings.
|
||||||
|
|
||||||
- Everything you get with self hosted Pangolin, but fully managed for you.
|
2. **Connect Private Sites**:
|
||||||
|
|
||||||
### Managed & High Availability
|
- Install Newt or use another WireGuard client on private sites.
|
||||||
|
- Automatically establish a connection from these sites to the central server.
|
||||||
|
|
||||||
Managed control plane, your infrastructure
|
3. **Expose Resources**:
|
||||||
|
|
||||||
- We manage database and control plane.
|
- Add resources to the central server and configure access control rules.
|
||||||
- You self-host lightweight exit-node.
|
- Access these resources securely from anywhere.
|
||||||
- Traffic flows through your infra.
|
|
||||||
- We coordinate failover between your nodes or to Cloud when things go bad.
|
|
||||||
|
|
||||||
Try it out using [Pangolin Cloud](https://pangolin.fossorial.io)
|
**Use Case Example - Bypassing Port Restrictions in Home Lab**:
|
||||||
|
Imagine private sites where the ISP restricts port forwarding. By connecting these sites to Pangolin via WireGuard, you can securely expose HTTP and HTTPS resources on the private network without any networking complexity.
|
||||||
|
|
||||||
### Full Enterprise On-Premises
|
**Use Case Example - Deploying Services For Your Business**:
|
||||||
|
You can use Pangolin as an easy way to expose your business applications to your users behind a safe authentication portal you can integrate into your IdP solution. Expose resources on prem and on the cloud.
|
||||||
|
|
||||||
[Contact us](mailto:numbat@fossorial.io) for a full distributed and enterprise deployments on your infrastructure controlled by your team.
|
**Use Case Example - IoT Networks**:
|
||||||
|
IoT networks are often fragmented and difficult to manage. By deploying Pangolin on a central server, you can connect all your IoT sites via Newt or another WireGuard client. This creates a simple, secure, and centralized way to access IoT resources without the need for intricate networking setups.
|
||||||
|
|
||||||
|
## Similar Projects and Inspirations
|
||||||
|
|
||||||
|
**Cloudflare Tunnels**:
|
||||||
|
A similar approach to proxying private resources securely, but Pangolin is a self-hosted alternative, giving you full control over your infrastructure.
|
||||||
|
|
||||||
|
**Authelia**:
|
||||||
|
This inspired Pangolin’s centralized authentication system for proxies, enabling robust user and role management.
|
||||||
|
|
||||||
## Project Development / Roadmap
|
## Project Development / Roadmap
|
||||||
|
|
||||||
We want to hear your feature requests! Add them to the [discussion board](https://github.com/orgs/fosrl/discussions/categories/feature-requests).
|
> [!NOTE]
|
||||||
|
> Pangolin is under heavy development. The roadmap is subject to change as we fix bugs, add new features, and make improvements.
|
||||||
|
|
||||||
|
View the [project board](https://github.com/orgs/fosrl/projects/1) for more detailed info.
|
||||||
|
|
||||||
## Licensing
|
## Licensing
|
||||||
|
|
||||||
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
Pangolin is dual licensed under the AGPL-3 and the Fossorial Commercial license. Please see the [LICENSE](./LICENSE) file in the repository for details. For inquiries about commercial licensing, please contact us at [numbat@fossorial.io](mailto:numbat@fossorial.io).
|
||||||
|
|
||||||
## Contributions
|
## Contributions
|
||||||
|
|
||||||
Looking for something to contribute? Take a look at issues marked with [help wanted](https://github.com/fosrl/pangolin/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22help%20wanted%22). Also take a look through the freature requests in Discussions - any are available and some are marked as a good first issue.
|
|
||||||
|
|
||||||
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
Please see [CONTRIBUTING](./CONTRIBUTING.md) in the repository for guidelines and best practices.
|
||||||
|
|
||||||
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
Please post bug reports and other functional issues in the [Issues](https://github.com/fosrl/pangolin/issues) section of the repository.
|
||||||
|
For all feature requests, or other ideas, please use the [Discussions](https://github.com/orgs/fosrl/discussions) section.
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
meta {
|
|
||||||
name: createClient
|
|
||||||
type: http
|
|
||||||
seq: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
put {
|
|
||||||
url: http://localhost:3000/api/v1/site/1/client
|
|
||||||
body: json
|
|
||||||
auth: none
|
|
||||||
}
|
|
||||||
|
|
||||||
body:json {
|
|
||||||
{
|
|
||||||
"siteId": 1,
|
|
||||||
"name": "test",
|
|
||||||
"type": "olm",
|
|
||||||
"subnet": "100.90.129.4/30",
|
|
||||||
"olmId": "029yzunhx6nh3y5",
|
|
||||||
"secret": "l0ymp075y3d4rccb25l6sqpgar52k09etunui970qq5gj7x6"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
meta {
|
|
||||||
name: pickClientDefaults
|
|
||||||
type: http
|
|
||||||
seq: 2
|
|
||||||
}
|
|
||||||
|
|
||||||
get {
|
|
||||||
url: http://localhost:3000/api/v1/site/1/pick-client-defaults
|
|
||||||
body: none
|
|
||||||
auth: none
|
|
||||||
}
|
|
|
@ -1,72 +0,0 @@
|
||||||
import { CommandModule } from "yargs";
|
|
||||||
import { db, users, securityKeys } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
type ResetUserSecurityKeysArgs = {
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetUserSecurityKeys: CommandModule<
|
|
||||||
{},
|
|
||||||
ResetUserSecurityKeysArgs
|
|
||||||
> = {
|
|
||||||
command: "reset-user-security-keys",
|
|
||||||
describe:
|
|
||||||
"Reset a user's security keys (passkeys) by deleting all their webauthn credentials",
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs.option("email", {
|
|
||||||
type: "string",
|
|
||||||
demandOption: true,
|
|
||||||
describe: "User email address"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (argv: { email: string }) => {
|
|
||||||
try {
|
|
||||||
const { email } = argv;
|
|
||||||
|
|
||||||
console.log(`Looking for user with email: ${email}`);
|
|
||||||
|
|
||||||
// Find the user by email
|
|
||||||
const [user] = await db
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.email, email))
|
|
||||||
.limit(1);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
console.error(`User with email '${email}' not found`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found user: ${user.email} (ID: ${user.userId})`);
|
|
||||||
|
|
||||||
// Check if user has any security keys
|
|
||||||
const userSecurityKeys = await db
|
|
||||||
.select()
|
|
||||||
.from(securityKeys)
|
|
||||||
.where(eq(securityKeys.userId, user.userId));
|
|
||||||
|
|
||||||
if (userSecurityKeys.length === 0) {
|
|
||||||
console.log(`User '${email}' has no security keys to reset`);
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(
|
|
||||||
`Found ${userSecurityKeys.length} security key(s) for user '${email}'`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete all security keys for the user
|
|
||||||
await db
|
|
||||||
.delete(securityKeys)
|
|
||||||
.where(eq(securityKeys.userId, user.userId));
|
|
||||||
|
|
||||||
console.log(`Successfully reset security keys for user '${email}'`);
|
|
||||||
console.log(`Deleted ${userSecurityKeys.length} security key(s)`);
|
|
||||||
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,143 +0,0 @@
|
||||||
import { CommandModule } from "yargs";
|
|
||||||
import { hashPassword, verifyPassword } from "@server/auth/password";
|
|
||||||
import { db, resourceSessions, sessions } from "@server/db";
|
|
||||||
import { users } from "@server/db";
|
|
||||||
import { eq, inArray } from "drizzle-orm";
|
|
||||||
import moment from "moment";
|
|
||||||
import { fromError } from "zod-validation-error";
|
|
||||||
import { passwordSchema } from "@server/auth/passwordSchema";
|
|
||||||
import { UserType } from "@server/types/UserTypes";
|
|
||||||
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
|
||||||
|
|
||||||
type SetAdminCredentialsArgs = {
|
|
||||||
email: string;
|
|
||||||
password: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setAdminCredentials: CommandModule<{}, SetAdminCredentialsArgs> = {
|
|
||||||
command: "set-admin-credentials",
|
|
||||||
describe: "Set the server admin credentials",
|
|
||||||
builder: (yargs) => {
|
|
||||||
return yargs
|
|
||||||
.option("email", {
|
|
||||||
type: "string",
|
|
||||||
demandOption: true,
|
|
||||||
describe: "Admin email address"
|
|
||||||
})
|
|
||||||
.option("password", {
|
|
||||||
type: "string",
|
|
||||||
demandOption: true,
|
|
||||||
describe: "Admin password"
|
|
||||||
});
|
|
||||||
},
|
|
||||||
handler: async (argv: { email: string; password: string }) => {
|
|
||||||
try {
|
|
||||||
const { password } = argv;
|
|
||||||
let { email } = argv;
|
|
||||||
email = email.trim().toLowerCase();
|
|
||||||
|
|
||||||
const parsed = passwordSchema.safeParse(password);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
throw Error(
|
|
||||||
`Invalid server admin password: ${fromError(parsed.error).toString()}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const passwordHash = await hashPassword(password);
|
|
||||||
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
try {
|
|
||||||
const [existing] = await trx
|
|
||||||
.select()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.serverAdmin, true));
|
|
||||||
|
|
||||||
if (existing) {
|
|
||||||
const passwordChanged = !(await verifyPassword(
|
|
||||||
password,
|
|
||||||
existing.passwordHash!
|
|
||||||
));
|
|
||||||
|
|
||||||
if (passwordChanged) {
|
|
||||||
await trx
|
|
||||||
.update(users)
|
|
||||||
.set({ passwordHash })
|
|
||||||
.where(eq(users.userId, existing.userId));
|
|
||||||
|
|
||||||
await invalidateAllSessions(existing.userId);
|
|
||||||
console.log("Server admin password updated");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (existing.email !== email) {
|
|
||||||
await trx
|
|
||||||
.update(users)
|
|
||||||
.set({ email, username: email })
|
|
||||||
.where(eq(users.userId, existing.userId));
|
|
||||||
|
|
||||||
console.log("Server admin email updated");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const userId = generateId(15);
|
|
||||||
|
|
||||||
await trx.update(users).set({ serverAdmin: false });
|
|
||||||
|
|
||||||
await db.insert(users).values({
|
|
||||||
userId: userId,
|
|
||||||
email: email,
|
|
||||||
type: UserType.Internal,
|
|
||||||
username: email,
|
|
||||||
passwordHash,
|
|
||||||
dateCreated: moment().toISOString(),
|
|
||||||
serverAdmin: true,
|
|
||||||
emailVerified: true
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Server admin created");
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Failed to set admin credentials", e);
|
|
||||||
trx.rollback();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log("Admin credentials updated successfully");
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error:", error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function invalidateAllSessions(userId: string): Promise<void> {
|
|
||||||
try {
|
|
||||||
await db.transaction(async (trx) => {
|
|
||||||
const userSessions = await trx
|
|
||||||
.select()
|
|
||||||
.from(sessions)
|
|
||||||
.where(eq(sessions.userId, userId));
|
|
||||||
await trx.delete(resourceSessions).where(
|
|
||||||
inArray(
|
|
||||||
resourceSessions.userSessionId,
|
|
||||||
userSessions.map((s) => s.sessionId)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await trx.delete(sessions).where(eq(sessions.userId, userId));
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
console.log("Failed to all invalidate user sessions", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const random: RandomReader = {
|
|
||||||
read(bytes: Uint8Array): void {
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export function generateId(length: number): string {
|
|
||||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
return generateRandomString(random, alphabet, length);
|
|
||||||
}
|
|
13
cli/index.ts
|
@ -1,13 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
import yargs from "yargs";
|
|
||||||
import { hideBin } from "yargs/helpers";
|
|
||||||
import { setAdminCredentials } from "@cli/commands/setAdminCredentials";
|
|
||||||
import { resetUserSecurityKeys } from "@cli/commands/resetUserSecurityKeys";
|
|
||||||
|
|
||||||
yargs(hideBin(process.argv))
|
|
||||||
.scriptName("pangctl")
|
|
||||||
.command(setAdminCredentials)
|
|
||||||
.command(resetUserSecurityKeys)
|
|
||||||
.demandCommand()
|
|
||||||
.help().argv;
|
|
|
@ -1,3 +0,0 @@
|
||||||
#!/bin/sh
|
|
||||||
cd /app/
|
|
||||||
./dist/cli.mjs "$@"
|
|
|
@ -1,28 +1,54 @@
|
||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
# https://docs.fossorial.io/Pangolin/Configuration/config
|
||||||
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: http://localhost:3002
|
dashboard_url: "http://localhost:3002"
|
||||||
log_level: debug
|
log_level: "info"
|
||||||
|
save_logs: false
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
base_domain: example.com
|
base_domain: "example.com"
|
||||||
|
cert_resolver: "letsencrypt"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: my_secret_key
|
external_port: 3000
|
||||||
|
internal_port: 3001
|
||||||
|
next_port: 3002
|
||||||
|
internal_hostname: "pangolin"
|
||||||
|
session_cookie_name: "p_session_token"
|
||||||
|
resource_access_token_param: "p_token"
|
||||||
|
secret: "your_secret_key_here"
|
||||||
|
resource_access_token_headers:
|
||||||
|
id: "P-Access-Token-Id"
|
||||||
|
token: "P-Access-Token"
|
||||||
|
resource_session_request_param: "p_session_request"
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
http_entrypoint: "web"
|
||||||
|
https_entrypoint: "websecure"
|
||||||
|
|
||||||
gerbil:
|
gerbil:
|
||||||
base_endpoint: example.com
|
start_port: 51820
|
||||||
|
base_endpoint: "localhost"
|
||||||
|
block_size: 24
|
||||||
|
site_block_size: 30
|
||||||
|
subnet_group: 100.89.137.0/20
|
||||||
|
use_subdomain: true
|
||||||
|
|
||||||
orgs:
|
rate_limits:
|
||||||
block_size: 24
|
global:
|
||||||
subnet_group: 100.90.137.0/20
|
window_minutes: 1
|
||||||
|
max_requests: 500
|
||||||
|
|
||||||
|
users:
|
||||||
|
server_admin:
|
||||||
|
email: "admin@example.com"
|
||||||
|
password: "Password123!"
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: false
|
require_email_verification: false
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: true
|
||||||
disable_user_create_org: true
|
disable_user_create_org: true
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
enable_integration_api: true
|
allow_base_domain_resources: true
|
||||||
enable_clients: true
|
|
||||||
|
|
|
@ -1,53 +0,0 @@
|
||||||
http:
|
|
||||||
middlewares:
|
|
||||||
redirect-to-https:
|
|
||||||
redirectScheme:
|
|
||||||
scheme: https
|
|
||||||
|
|
||||||
routers:
|
|
||||||
# HTTP to HTTPS redirect router
|
|
||||||
main-app-router-redirect:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
|
||||||
service: next-service
|
|
||||||
entryPoints:
|
|
||||||
- web
|
|
||||||
middlewares:
|
|
||||||
- redirect-to-https
|
|
||||||
|
|
||||||
# Next.js router (handles everything except API and WebSocket paths)
|
|
||||||
next-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && !PathPrefix(`/api/v1`)"
|
|
||||||
service: next-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
# API router (handles /api/v1 paths)
|
|
||||||
api-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`) && PathPrefix(`/api/v1`)"
|
|
||||||
service: api-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
# WebSocket router
|
|
||||||
ws-router:
|
|
||||||
rule: "Host(`{{.DashboardDomain}}`)"
|
|
||||||
service: api-service
|
|
||||||
entryPoints:
|
|
||||||
- websecure
|
|
||||||
tls:
|
|
||||||
certResolver: letsencrypt
|
|
||||||
|
|
||||||
services:
|
|
||||||
next-service:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://pangolin:3002" # Next.js server
|
|
||||||
|
|
||||||
api-service:
|
|
||||||
loadBalancer:
|
|
||||||
servers:
|
|
||||||
- url: "http://pangolin:3000" # API/WebSocket server
|
|
|
@ -1,34 +0,0 @@
|
||||||
api:
|
|
||||||
insecure: true
|
|
||||||
dashboard: true
|
|
||||||
|
|
||||||
providers:
|
|
||||||
file:
|
|
||||||
directory: "/var/dynamic"
|
|
||||||
watch: true
|
|
||||||
|
|
||||||
experimental:
|
|
||||||
plugins:
|
|
||||||
badger:
|
|
||||||
moduleName: "github.com/fosrl/badger"
|
|
||||||
version: "v1.2.0"
|
|
||||||
|
|
||||||
log:
|
|
||||||
level: "DEBUG"
|
|
||||||
format: "common"
|
|
||||||
maxSize: 100
|
|
||||||
maxBackups: 3
|
|
||||||
maxAge: 3
|
|
||||||
compress: true
|
|
||||||
|
|
||||||
entryPoints:
|
|
||||||
web:
|
|
||||||
address: ":80"
|
|
||||||
websecure:
|
|
||||||
address: ":9443"
|
|
||||||
transport:
|
|
||||||
respondingTimeouts:
|
|
||||||
readTimeout: "30m"
|
|
||||||
|
|
||||||
serversTransport:
|
|
||||||
insecureSkipVerify: true
|
|
|
@ -1,3 +0,0 @@
|
||||||
files:
|
|
||||||
- source: /messages/en-US.json
|
|
||||||
translation: /messages/%locale%.json
|
|
|
@ -22,7 +22,8 @@ services:
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3003
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
||||||
|
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/:/var/config
|
- ./config/:/var/config
|
||||||
cap_add:
|
cap_add:
|
||||||
|
@ -30,12 +31,11 @@ services:
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
|
||||||
- 443:443 # Port for traefik because of the network_mode
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.5
|
image: traefik:v3.4.0
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
@ -52,4 +52,3 @@ networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
enable_ipv6: true
|
|
|
@ -1,14 +0,0 @@
|
||||||
services:
|
|
||||||
# PostgreSQL Service
|
|
||||||
db:
|
|
||||||
image: postgres:17 # Use the PostgreSQL 17 image
|
|
||||||
container_name: dev_postgres # Name your PostgreSQL container
|
|
||||||
environment:
|
|
||||||
POSTGRES_DB: postgres # Default database name
|
|
||||||
POSTGRES_USER: postgres # Default user
|
|
||||||
POSTGRES_PASSWORD: password # Default password (change for production!)
|
|
||||||
volumes:
|
|
||||||
- ./config/postgres:/var/lib/postgresql/data
|
|
||||||
ports:
|
|
||||||
- "5432:5432" # Map host port 5432 to container port 5432
|
|
||||||
restart: no
|
|
|
@ -1,32 +0,0 @@
|
||||||
name: pangolin
|
|
||||||
services:
|
|
||||||
gerbil:
|
|
||||||
image: gerbil
|
|
||||||
container_name: gerbil
|
|
||||||
network_mode: host
|
|
||||||
restart: unless-stopped
|
|
||||||
command:
|
|
||||||
- --reachableAt=http://localhost:3003
|
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
|
||||||
- --remoteConfig=http://localhost:3001/api/v1/
|
|
||||||
- --sni-port=443
|
|
||||||
volumes:
|
|
||||||
- ./config/:/var/config
|
|
||||||
cap_add:
|
|
||||||
- NET_ADMIN
|
|
||||||
- SYS_MODULE
|
|
||||||
|
|
||||||
traefik:
|
|
||||||
image: docker.io/traefik:v3.4.1
|
|
||||||
container_name: traefik
|
|
||||||
restart: unless-stopped
|
|
||||||
network_mode: host
|
|
||||||
command:
|
|
||||||
- --configFile=/etc/traefik/traefik_config.yml
|
|
||||||
volumes:
|
|
||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
|
||||||
- ./certificates:/var/certificates:ro
|
|
||||||
- ./dynamic:/var/dynamic:ro
|
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
services:
|
|
||||||
# Development application service
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.dev
|
|
||||||
container_name: dev_pangolin
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
- "3001:3001"
|
|
||||||
- "3002:3002"
|
|
||||||
- "3003:3003"
|
|
||||||
environment:
|
|
||||||
- NODE_ENV=development
|
|
||||||
- ENVIRONMENT=dev
|
|
||||||
- DB_TYPE=pg
|
|
||||||
volumes:
|
|
||||||
# Mount source code for hot reload
|
|
||||||
- ./src:/app/src
|
|
||||||
- ./server:/app/server
|
|
||||||
- ./public:/app/public
|
|
||||||
- ./messages:/app/messages
|
|
||||||
- ./components.json:/app/components.json
|
|
||||||
- ./next.config.mjs:/app/next.config.mjs
|
|
||||||
- ./tsconfig.json:/app/tsconfig.json
|
|
||||||
- ./tailwind.config.js:/app/tailwind.config.js
|
|
||||||
- ./postcss.config.mjs:/app/postcss.config.mjs
|
|
||||||
- ./eslint.config.js:/app/eslint.config.js
|
|
||||||
- ./config:/app/config
|
|
||||||
restart: no
|
|
|
@ -3,7 +3,7 @@ import path from "path";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
schema: [path.join("server", "db", "pg", "schema.ts")],
|
schema: path.join("server", "db", "pg", "schema.ts"),
|
||||||
out: path.join("server", "migrations"),
|
out: path.join("server", "migrations"),
|
||||||
verbose: true,
|
verbose: true,
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
|
|
|
@ -63,8 +63,8 @@ esbuild
|
||||||
packagePath: getPackagePaths(),
|
packagePath: getPackagePaths(),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
sourcemap: "external",
|
sourcemap: true,
|
||||||
target: "node22",
|
target: "node20",
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
console.log("Build completed successfully");
|
console.log("Build completed successfully");
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
import tseslint from 'typescript-eslint';
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
export default tseslint.config({
|
export default tseslint.config(
|
||||||
files: ["**/*.{ts,tsx,js,jsx}"],
|
tseslint.configs.recommended,
|
||||||
languageOptions: {
|
{
|
||||||
parser: tseslint.parser,
|
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
|
||||||
parserOptions: {
|
rules: {
|
||||||
ecmaVersion: "latest",
|
semi: "error",
|
||||||
sourceType: "module",
|
"prefer-const": "error"
|
||||||
ecmaFeatures: {
|
}
|
||||||
jsx: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
);
|
||||||
rules: {
|
|
||||||
"semi": "error",
|
|
||||||
"prefer-const": "warn"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
all: update-versions go-build-release put-back
|
all: update-versions go-build-release put-back
|
||||||
dev-all: dev-update-versions dev-build dev-clean
|
|
||||||
|
|
||||||
go-build-release:
|
go-build-release:
|
||||||
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64
|
||||||
|
@ -12,12 +11,6 @@ clean:
|
||||||
update-versions:
|
update-versions:
|
||||||
@echo "Fetching latest versions..."
|
@echo "Fetching latest versions..."
|
||||||
cp main.go main.go.bak && \
|
cp main.go main.go.bak && \
|
||||||
$(MAKE) dev-update-versions
|
|
||||||
|
|
||||||
put-back:
|
|
||||||
mv main.go.bak main.go
|
|
||||||
|
|
||||||
dev-update-versions:
|
|
||||||
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') && \
|
||||||
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \
|
||||||
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \
|
||||||
|
@ -27,11 +20,5 @@ dev-update-versions:
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \
|
||||||
echo "Updated main.go with latest versions"
|
echo "Updated main.go with latest versions"
|
||||||
|
|
||||||
dev-build: go-build-release
|
put-back:
|
||||||
|
mv main.go.bak main.go
|
||||||
dev-clean:
|
|
||||||
@echo "Restoring version values ..."
|
|
||||||
sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \
|
|
||||||
sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go
|
|
||||||
@echo "Restored version strings in main.go"
|
|
|
@ -37,28 +37,15 @@ type DynamicConfig struct {
|
||||||
} `yaml:"http"`
|
} `yaml:"http"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TraefikConfigValues holds the extracted configuration values
|
// ConfigValues holds the extracted configuration values
|
||||||
type TraefikConfigValues struct {
|
type ConfigValues struct {
|
||||||
DashboardDomain string
|
DashboardDomain string
|
||||||
LetsEncryptEmail string
|
LetsEncryptEmail string
|
||||||
BadgerVersion string
|
BadgerVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
// AppConfig represents the app section of the config.yml
|
|
||||||
type AppConfig struct {
|
|
||||||
App struct {
|
|
||||||
DashboardURL string `yaml:"dashboard_url"`
|
|
||||||
LogLevel string `yaml:"log_level"`
|
|
||||||
} `yaml:"app"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AppConfigValues struct {
|
|
||||||
DashboardURL string
|
|
||||||
LogLevel string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReadTraefikConfig reads and extracts values from Traefik configuration files
|
// ReadTraefikConfig reads and extracts values from Traefik configuration files
|
||||||
func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) {
|
func ReadTraefikConfig(mainConfigPath, dynamicConfigPath string) (*ConfigValues, error) {
|
||||||
// Read main config file
|
// Read main config file
|
||||||
mainConfigData, err := os.ReadFile(mainConfigPath)
|
mainConfigData, err := os.ReadFile(mainConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -70,33 +57,48 @@ func ReadTraefikConfig(mainConfigPath string) (*TraefikConfigValues, error) {
|
||||||
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
return nil, fmt.Errorf("error parsing main config file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read dynamic config file
|
||||||
|
dynamicConfigData, err := os.ReadFile(dynamicConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading dynamic config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var dynamicConfig DynamicConfig
|
||||||
|
if err := yaml.Unmarshal(dynamicConfigData, &dynamicConfig); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing dynamic config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Extract values
|
// Extract values
|
||||||
values := &TraefikConfigValues{
|
values := &ConfigValues{
|
||||||
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
BadgerVersion: mainConfig.Experimental.Plugins.Badger.Version,
|
||||||
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
LetsEncryptEmail: mainConfig.CertificatesResolvers.LetsEncrypt.Acme.Email,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract DashboardDomain from router rules
|
||||||
|
// Look for it in the main router rules
|
||||||
|
for _, router := range dynamicConfig.HTTP.Routers {
|
||||||
|
if router.Rule != "" {
|
||||||
|
// Extract domain from Host(`mydomain.com`)
|
||||||
|
if domain := extractDomainFromRule(router.Rule); domain != "" {
|
||||||
|
values.DashboardDomain = domain
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return values, nil
|
return values, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ReadAppConfig(configPath string) (*AppConfigValues, error) {
|
// extractDomainFromRule extracts the domain from a router rule
|
||||||
// Read config file
|
func extractDomainFromRule(rule string) string {
|
||||||
configData, err := os.ReadFile(configPath)
|
// Look for the Host(`mydomain.com`) pattern
|
||||||
if err != nil {
|
if start := findPattern(rule, "Host(`"); start != -1 {
|
||||||
return nil, fmt.Errorf("error reading config file: %w", err)
|
end := findPattern(rule[start:], "`)")
|
||||||
|
if end != -1 {
|
||||||
|
return rule[start+6 : start+end]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
return ""
|
||||||
var appConfig AppConfig
|
|
||||||
if err := yaml.Unmarshal(configData, &appConfig); err != nil {
|
|
||||||
return nil, fmt.Errorf("error parsing config file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
values := &AppConfigValues{
|
|
||||||
DashboardURL: appConfig.App.DashboardURL,
|
|
||||||
LogLevel: appConfig.App.LogLevel,
|
|
||||||
}
|
|
||||||
|
|
||||||
return values, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// findPattern finds the start of a pattern in a string
|
// findPattern finds the start of a pattern in a string
|
||||||
|
|
|
@ -1,20 +1,10 @@
|
||||||
# To see all available options, please visit the docs:
|
# To see all available options, please visit the docs:
|
||||||
# https://docs.digpangolin.com/self-host/advanced/config-file
|
# https://docs.fossorial.io/Pangolin/Configuration/config
|
||||||
|
|
||||||
gerbil:
|
|
||||||
start_port: 51820
|
|
||||||
base_endpoint: "{{.DashboardDomain}}"
|
|
||||||
{{if .HybridMode}}
|
|
||||||
managed:
|
|
||||||
id: "{{.HybridId}}"
|
|
||||||
secret: "{{.HybridSecret}}"
|
|
||||||
|
|
||||||
{{else}}
|
|
||||||
app:
|
app:
|
||||||
dashboard_url: "https://{{.DashboardDomain}}"
|
dashboard_url: "https://{{.DashboardDomain}}"
|
||||||
log_level: "info"
|
log_level: "info"
|
||||||
telemetry:
|
save_logs: false
|
||||||
anonymous_usage: true
|
|
||||||
|
|
||||||
domains:
|
domains:
|
||||||
domain1:
|
domain1:
|
||||||
|
@ -22,12 +12,40 @@ domains:
|
||||||
cert_resolver: "letsencrypt"
|
cert_resolver: "letsencrypt"
|
||||||
|
|
||||||
server:
|
server:
|
||||||
secret: "{{.Secret}}"
|
external_port: 3000
|
||||||
|
internal_port: 3001
|
||||||
|
next_port: 3002
|
||||||
|
internal_hostname: "pangolin"
|
||||||
|
session_cookie_name: "p_session_token"
|
||||||
|
resource_access_token_param: "p_token"
|
||||||
|
resource_access_token_headers:
|
||||||
|
id: "P-Access-Token-Id"
|
||||||
|
token: "P-Access-Token"
|
||||||
|
resource_session_request_param: "p_session_request"
|
||||||
|
secret: {{.Secret}}
|
||||||
cors:
|
cors:
|
||||||
origins: ["https://{{.DashboardDomain}}"]
|
origins: ["https://{{.DashboardDomain}}"]
|
||||||
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||||
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
allowed_headers: ["X-CSRF-Token", "Content-Type"]
|
||||||
credentials: false
|
credentials: false
|
||||||
|
|
||||||
|
traefik:
|
||||||
|
cert_resolver: "letsencrypt"
|
||||||
|
http_entrypoint: "web"
|
||||||
|
https_entrypoint: "websecure"
|
||||||
|
|
||||||
|
gerbil:
|
||||||
|
start_port: 51820
|
||||||
|
base_endpoint: "{{.DashboardDomain}}"
|
||||||
|
use_subdomain: false
|
||||||
|
block_size: 24
|
||||||
|
site_block_size: 30
|
||||||
|
subnet_group: 100.89.137.0/20
|
||||||
|
|
||||||
|
rate_limits:
|
||||||
|
global:
|
||||||
|
window_minutes: 1
|
||||||
|
max_requests: 500
|
||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
|
@ -36,9 +54,14 @@ email:
|
||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
{{end}}
|
{{end}}
|
||||||
|
users:
|
||||||
|
server_admin:
|
||||||
|
email: "{{.AdminUserEmail}}"
|
||||||
|
password: "{{.AdminUserPassword}}"
|
||||||
|
|
||||||
flags:
|
flags:
|
||||||
require_email_verification: {{.EnableEmail}}
|
require_email_verification: {{.EnableEmail}}
|
||||||
disable_signup_without_invite: true
|
disable_signup_without_invite: {{.DisableSignupWithoutInvite}}
|
||||||
disable_user_create_org: false
|
disable_user_create_org: {{.DisableUserCreateOrg}}
|
||||||
allow_raw_resources: true
|
allow_raw_resources: true
|
||||||
{{end}}
|
allow_base_domain_resources: true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
services:
|
services:
|
||||||
crowdsec:
|
crowdsec:
|
||||||
image: docker.io/crowdsecurity/crowdsec:latest
|
image: crowdsecurity/crowdsec:latest
|
||||||
container_name: crowdsec
|
container_name: crowdsec
|
||||||
environment:
|
environment:
|
||||||
GID: "1000"
|
GID: "1000"
|
||||||
|
|
|
@ -16,7 +16,7 @@ experimental:
|
||||||
version: "{{.BadgerVersion}}"
|
version: "{{.BadgerVersion}}"
|
||||||
crowdsec: # CrowdSec plugin configuration added
|
crowdsec: # CrowdSec plugin configuration added
|
||||||
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
|
||||||
version: "v1.4.4"
|
version: "v1.4.2"
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: "INFO"
|
level: "INFO"
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
name: pangolin
|
name: pangolin
|
||||||
services:
|
services:
|
||||||
pangolin:
|
pangolin:
|
||||||
image: docker.io/fosrl/pangolin:{{.PangolinVersion}}
|
image: fosrl/pangolin:{{.PangolinVersion}}
|
||||||
container_name: pangolin
|
container_name: pangolin
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
- ./config:/app/config
|
- ./config:/app/config
|
||||||
- pangolin-data:/var/certificates
|
|
||||||
- pangolin-data:/var/dynamic
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
|
||||||
interval: "10s"
|
interval: "10s"
|
||||||
|
@ -15,7 +13,7 @@ services:
|
||||||
retries: 15
|
retries: 15
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
gerbil:
|
gerbil:
|
||||||
image: docker.io/fosrl/gerbil:{{.GerbilVersion}}
|
image: fosrl/gerbil:{{.GerbilVersion}}
|
||||||
container_name: gerbil
|
container_name: gerbil
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
depends_on:
|
depends_on:
|
||||||
|
@ -24,7 +22,8 @@ services:
|
||||||
command:
|
command:
|
||||||
- --reachableAt=http://gerbil:3003
|
- --reachableAt=http://gerbil:3003
|
||||||
- --generateAndSaveKeyTo=/var/config/key
|
- --generateAndSaveKeyTo=/var/config/key
|
||||||
- --remoteConfig=http://pangolin:3001/api/v1/
|
- --remoteConfig=http://pangolin:3001/api/v1/gerbil/get-config
|
||||||
|
- --reportBandwidthTo=http://pangolin:3001/api/v1/gerbil/receive-bandwidth
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/:/var/config
|
- ./config/:/var/config
|
||||||
cap_add:
|
cap_add:
|
||||||
|
@ -32,12 +31,11 @@ services:
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
ports:
|
ports:
|
||||||
- 51820:51820/udp
|
- 51820:51820/udp
|
||||||
- 21820:21820/udp
|
- 443:443 # Port for traefik because of the network_mode
|
||||||
- 443:{{if .HybridMode}}8443{{else}}443{{end}}
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
- 80:80
|
|
||||||
{{end}}
|
{{end}}
|
||||||
traefik:
|
traefik:
|
||||||
image: docker.io/traefik:v3.5
|
image: traefik:v3.4.1
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
|
@ -56,15 +54,8 @@ services:
|
||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
- ./config/traefik/logs:/var/log/traefik # Volume to store Traefik logs
|
||||||
# Shared volume for certificates and dynamic config in file mode
|
|
||||||
- pangolin-data:/var/certificates:ro
|
|
||||||
- pangolin-data:/var/dynamic:ro
|
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
name: pangolin
|
name: pangolin
|
||||||
{{if .EnableIPv6}} enable_ipv6: true{{end}}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
pangolin-data:
|
|
||||||
|
|
|
@ -3,17 +3,12 @@ api:
|
||||||
dashboard: true
|
dashboard: true
|
||||||
|
|
||||||
providers:
|
providers:
|
||||||
{{if not .HybridMode}}
|
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
{{else}}
|
|
||||||
file:
|
|
||||||
directory: "/var/dynamic"
|
|
||||||
watch: true
|
|
||||||
{{end}}
|
|
||||||
experimental:
|
experimental:
|
||||||
plugins:
|
plugins:
|
||||||
badger:
|
badger:
|
||||||
|
@ -27,7 +22,7 @@ log:
|
||||||
maxBackups: 3
|
maxBackups: 3
|
||||||
maxAge: 3
|
maxAge: 3
|
||||||
compress: true
|
compress: true
|
||||||
{{if not .HybridMode}}
|
|
||||||
certificatesResolvers:
|
certificatesResolvers:
|
||||||
letsencrypt:
|
letsencrypt:
|
||||||
acme:
|
acme:
|
||||||
|
@ -36,25 +31,18 @@ certificatesResolvers:
|
||||||
email: "{{.LetsEncryptEmail}}"
|
email: "{{.LetsEncryptEmail}}"
|
||||||
storage: "/letsencrypt/acme.json"
|
storage: "/letsencrypt/acme.json"
|
||||||
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
caServer: "https://acme-v02.api.letsencrypt.org/directory"
|
||||||
{{end}}
|
|
||||||
entryPoints:
|
entryPoints:
|
||||||
web:
|
web:
|
||||||
address: ":80"
|
address: ":80"
|
||||||
websecure:
|
websecure:
|
||||||
address: ":443"
|
address: ":443"
|
||||||
{{if .HybridMode}} proxyProtocol:
|
|
||||||
trustedIPs:
|
|
||||||
- 0.0.0.0/0
|
|
||||||
- ::1/128{{end}}
|
|
||||||
transport:
|
transport:
|
||||||
respondingTimeouts:
|
respondingTimeouts:
|
||||||
readTimeout: "30m"
|
readTimeout: "30m"
|
||||||
{{if not .HybridMode}} http:
|
http:
|
||||||
tls:
|
tls:
|
||||||
certResolver: "letsencrypt"{{end}}
|
certResolver: "letsencrypt"
|
||||||
|
|
||||||
serversTransport:
|
serversTransport:
|
||||||
insecureSkipVerify: true
|
insecureSkipVerify: true
|
||||||
|
|
||||||
ping:
|
|
||||||
entryPoint: "web"
|
|
|
@ -1,332 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"os/user"
|
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func waitForContainer(containerName string, containerType SupportedContainer) error {
|
|
||||||
maxAttempts := 30
|
|
||||||
retryInterval := time.Second * 2
|
|
||||||
|
|
||||||
for attempt := 0; attempt < maxAttempts; attempt++ {
|
|
||||||
// Check if container is running
|
|
||||||
cmd := exec.Command(string(containerType), "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
|
||||||
var out bytes.Buffer
|
|
||||||
cmd.Stdout = &out
|
|
||||||
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
// If the container doesn't exist or there's another error, wait and retry
|
|
||||||
time.Sleep(retryInterval)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
isRunning := strings.TrimSpace(out.String()) == "true"
|
|
||||||
if isRunning {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Container exists but isn't running yet, wait and retry
|
|
||||||
time.Sleep(retryInterval)
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
|
||||||
}
|
|
||||||
|
|
||||||
func installDocker() error {
|
|
||||||
// Detect Linux distribution
|
|
||||||
cmd := exec.Command("cat", "/etc/os-release")
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to detect Linux distribution: %v", err)
|
|
||||||
}
|
|
||||||
osRelease := string(output)
|
|
||||||
|
|
||||||
// Detect system architecture
|
|
||||||
archCmd := exec.Command("uname", "-m")
|
|
||||||
archOutput, err := archCmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to detect system architecture: %v", err)
|
|
||||||
}
|
|
||||||
arch := strings.TrimSpace(string(archOutput))
|
|
||||||
|
|
||||||
// Map architecture to Docker's architecture naming
|
|
||||||
var dockerArch string
|
|
||||||
switch arch {
|
|
||||||
case "x86_64":
|
|
||||||
dockerArch = "amd64"
|
|
||||||
case "aarch64":
|
|
||||||
dockerArch = "arm64"
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported architecture: %s", arch)
|
|
||||||
}
|
|
||||||
|
|
||||||
var installCmd *exec.Cmd
|
|
||||||
switch {
|
|
||||||
case strings.Contains(osRelease, "ID=ubuntu"):
|
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
||||||
apt-get update &&
|
|
||||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
|
||||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
|
||||||
apt-get update &&
|
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
||||||
`, dockerArch))
|
|
||||||
case strings.Contains(osRelease, "ID=debian"):
|
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
||||||
apt-get update &&
|
|
||||||
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
|
||||||
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
|
||||||
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
|
||||||
apt-get update &&
|
|
||||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
||||||
`, dockerArch))
|
|
||||||
case strings.Contains(osRelease, "ID=fedora"):
|
|
||||||
// Detect Fedora version to handle DNF 5 changes
|
|
||||||
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
|
|
||||||
versionOutput, err := versionCmd.Output()
|
|
||||||
var fedoraVersion int
|
|
||||||
if err == nil {
|
|
||||||
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
|
|
||||||
fedoraVersion = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use appropriate DNF syntax based on version
|
|
||||||
var repoCmd string
|
|
||||||
if fedoraVersion >= 41 {
|
|
||||||
// DNF 5 syntax for Fedora 41+
|
|
||||||
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
|
|
||||||
} else {
|
|
||||||
// DNF 4 syntax for Fedora < 41
|
|
||||||
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
|
||||||
dnf -y install dnf-plugins-core &&
|
|
||||||
%s &&
|
|
||||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
|
||||||
`, repoCmd))
|
|
||||||
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
|
||||||
installCmd = exec.Command("bash", "-c", `
|
|
||||||
zypper install -y docker docker-compose &&
|
|
||||||
systemctl enable docker
|
|
||||||
`)
|
|
||||||
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
|
||||||
installCmd = exec.Command("bash", "-c", `
|
|
||||||
dnf remove -y runc &&
|
|
||||||
dnf -y install yum-utils &&
|
|
||||||
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
|
||||||
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
|
||||||
systemctl enable docker
|
|
||||||
`)
|
|
||||||
case strings.Contains(osRelease, "ID=amzn"):
|
|
||||||
installCmd = exec.Command("bash", "-c", `
|
|
||||||
yum update -y &&
|
|
||||||
yum install -y docker &&
|
|
||||||
systemctl enable docker &&
|
|
||||||
usermod -a -G docker ec2-user
|
|
||||||
`)
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("unsupported Linux distribution")
|
|
||||||
}
|
|
||||||
|
|
||||||
installCmd.Stdout = os.Stdout
|
|
||||||
installCmd.Stderr = os.Stderr
|
|
||||||
return installCmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func startDockerService() error {
|
|
||||||
if runtime.GOOS == "linux" {
|
|
||||||
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
} else if runtime.GOOS == "darwin" {
|
|
||||||
// On macOS, Docker is usually started via the Docker Desktop application
|
|
||||||
fmt.Println("Please start Docker Desktop manually on macOS.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("unsupported operating system for starting Docker service")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isDockerInstalled() bool {
|
|
||||||
return isContainerInstalled("docker")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isPodmanInstalled() bool {
|
|
||||||
return isContainerInstalled("podman") && isContainerInstalled("podman-compose")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isContainerInstalled(container string) bool {
|
|
||||||
cmd := exec.Command(container, "--version")
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func isUserInDockerGroup() bool {
|
|
||||||
if runtime.GOOS == "darwin" {
|
|
||||||
// Docker group is not applicable on macOS
|
|
||||||
// So we assume that the user can run Docker commands
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
if os.Geteuid() == 0 {
|
|
||||||
return true // Root user can run Docker commands anyway
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if the current user is in the docker group
|
|
||||||
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
|
|
||||||
if currentUser, err := user.Current(); err == nil {
|
|
||||||
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
|
|
||||||
for _, groupId := range currentUserGroupIds {
|
|
||||||
if groupId == dockerGroup.Gid {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
|
|
||||||
func isDockerRunning() bool {
|
|
||||||
cmd := exec.Command("docker", "info")
|
|
||||||
if err := cmd.Run(); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
|
||||||
func executeDockerComposeCommandWithArgs(args ...string) error {
|
|
||||||
var cmd *exec.Cmd
|
|
||||||
var useNewStyle bool
|
|
||||||
|
|
||||||
if !isDockerInstalled() {
|
|
||||||
return fmt.Errorf("docker is not installed")
|
|
||||||
}
|
|
||||||
|
|
||||||
checkCmd := exec.Command("docker", "compose", "version")
|
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
useNewStyle = true
|
|
||||||
} else {
|
|
||||||
checkCmd = exec.Command("docker-compose", "version")
|
|
||||||
if err := checkCmd.Run(); err == nil {
|
|
||||||
useNewStyle = false
|
|
||||||
} else {
|
|
||||||
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if useNewStyle {
|
|
||||||
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("docker-compose", args...)
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
// pullContainers pulls the containers using the appropriate command.
|
|
||||||
func pullContainers(containerType SupportedContainer) error {
|
|
||||||
fmt.Println("Pulling the container images...")
|
|
||||||
if containerType == Podman {
|
|
||||||
if err := run("podman-compose", "-f", "docker-compose.yml", "pull"); err != nil {
|
|
||||||
return fmt.Errorf("failed to pull the containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerType == Docker {
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
|
||||||
return fmt.Errorf("failed to pull the containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// startContainers starts the containers using the appropriate command.
|
|
||||||
func startContainers(containerType SupportedContainer) error {
|
|
||||||
fmt.Println("Starting containers...")
|
|
||||||
|
|
||||||
if containerType == Podman {
|
|
||||||
if err := run("podman-compose", "-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
|
||||||
return fmt.Errorf("failed start containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerType == Docker {
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stopContainers stops the containers using the appropriate command.
|
|
||||||
func stopContainers(containerType SupportedContainer) error {
|
|
||||||
fmt.Println("Stopping containers...")
|
|
||||||
if containerType == Podman {
|
|
||||||
if err := run("podman-compose", "-f", "docker-compose.yml", "down"); err != nil {
|
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerType == Docker {
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restartContainer restarts a specific container using the appropriate command.
|
|
||||||
func restartContainer(container string, containerType SupportedContainer) error {
|
|
||||||
fmt.Println("Restarting containers...")
|
|
||||||
if containerType == Podman {
|
|
||||||
if err := run("podman-compose", "-f", "docker-compose.yml", "restart"); err != nil {
|
|
||||||
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if containerType == Docker {
|
|
||||||
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
|
||||||
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Unsupported container type: %s", containerType)
|
|
||||||
}
|
|
|
@ -13,7 +13,7 @@ import (
|
||||||
|
|
||||||
func installCrowdsec(config Config) error {
|
func installCrowdsec(config Config) error {
|
||||||
|
|
||||||
if err := stopContainers(config.InstallationContainerType); err != nil {
|
if err := stopContainers(); err != nil {
|
||||||
return fmt.Errorf("failed to stop containers: %v", err)
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,12 +72,12 @@ func installCrowdsec(config Config) error {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := startContainers(config.InstallationContainerType); err != nil {
|
if err := startContainers(); err != nil {
|
||||||
return fmt.Errorf("failed to start containers: %v", err)
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// get API key
|
// get API key
|
||||||
apiKey, err := GetCrowdSecAPIKey(config.InstallationContainerType)
|
apiKey, err := GetCrowdSecAPIKey()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get API key: %v", err)
|
return fmt.Errorf("failed to get API key: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -87,7 +87,7 @@ func installCrowdsec(config Config) error {
|
||||||
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
return fmt.Errorf("failed to replace bouncer key: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := restartContainer("traefik", config.InstallationContainerType); err != nil {
|
if err := restartContainer("traefik"); err != nil {
|
||||||
return fmt.Errorf("failed to restart containers: %v", err)
|
return fmt.Errorf("failed to restart containers: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -110,9 +110,9 @@ func checkIsCrowdsecInstalledInCompose() bool {
|
||||||
return bytes.Contains(content, []byte("crowdsec:"))
|
return bytes.Contains(content, []byte("crowdsec:"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetCrowdSecAPIKey(containerType SupportedContainer) (string, error) {
|
func GetCrowdSecAPIKey() (string, error) {
|
||||||
// First, ensure the container is running
|
// First, ensure the container is running
|
||||||
if err := waitForContainer("crowdsec", containerType); err != nil {
|
if err := waitForContainer("crowdsec"); err != nil {
|
||||||
return "", fmt.Errorf("waiting for container: %w", err)
|
return "", fmt.Errorf("waiting for container: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
module installer
|
module installer
|
||||||
|
|
||||||
go 1.24
|
go 1.23.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
golang.org/x/term v0.34.0
|
golang.org/x/term v0.28.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require golang.org/x/sys v0.35.0 // indirect
|
require golang.org/x/sys v0.29.0 // indirect
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
|
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||||
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"golang.org/x/term"
|
|
||||||
)
|
|
||||||
|
|
||||||
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
|
||||||
if defaultValue != "" {
|
|
||||||
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
|
||||||
} else {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
}
|
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
input = strings.TrimSpace(input)
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|
||||||
func readStringNoDefault(reader *bufio.Reader, prompt string) string {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
input, _ := reader.ReadString('\n')
|
|
||||||
return strings.TrimSpace(input)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readPassword(prompt string, reader *bufio.Reader) string {
|
|
||||||
if term.IsTerminal(int(syscall.Stdin)) {
|
|
||||||
fmt.Print(prompt + ": ")
|
|
||||||
// Read password without echo if we're in a terminal
|
|
||||||
password, err := term.ReadPassword(int(syscall.Stdin))
|
|
||||||
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
input := strings.TrimSpace(string(password))
|
|
||||||
if input == "" {
|
|
||||||
return readPassword(prompt, reader)
|
|
||||||
}
|
|
||||||
return input
|
|
||||||
} else {
|
|
||||||
// Fallback to reading from stdin if not in a terminal
|
|
||||||
return readString(reader, prompt, "")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
|
||||||
defaultStr := "no"
|
|
||||||
if defaultValue {
|
|
||||||
defaultStr = "yes"
|
|
||||||
}
|
|
||||||
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func readBoolNoDefault(reader *bufio.Reader, prompt string) bool {
|
|
||||||
input := readStringNoDefault(reader, prompt+" (yes/no)")
|
|
||||||
return strings.ToLower(input) == "yes"
|
|
||||||
}
|
|
||||||
|
|
||||||
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
|
||||||
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
|
||||||
if input == "" {
|
|
||||||
return defaultValue
|
|
||||||
}
|
|
||||||
value := defaultValue
|
|
||||||
fmt.Sscanf(input, "%d", &value)
|
|
||||||
return value
|
|
||||||
}
|
|
|
@ -1,7 +1,5 @@
|
||||||
docker
|
|
||||||
example.com
|
example.com
|
||||||
pangolin.example.com
|
pangolin.example.com
|
||||||
yes
|
|
||||||
admin@example.com
|
admin@example.com
|
||||||
yes
|
yes
|
||||||
admin@example.com
|
admin@example.com
|
||||||
|
|
865
install/main.go
|
@ -7,15 +7,20 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"os/user"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
"net"
|
"unicode"
|
||||||
|
"math/rand"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/term"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD
|
||||||
|
@ -29,58 +34,45 @@ func loadVersions(config *Config) {
|
||||||
var configFiles embed.FS
|
var configFiles embed.FS
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
InstallationContainerType SupportedContainer
|
PangolinVersion string
|
||||||
PangolinVersion string
|
GerbilVersion string
|
||||||
GerbilVersion string
|
BadgerVersion string
|
||||||
BadgerVersion string
|
BaseDomain string
|
||||||
BaseDomain string
|
DashboardDomain string
|
||||||
DashboardDomain string
|
LetsEncryptEmail string
|
||||||
EnableIPv6 bool
|
AdminUserEmail string
|
||||||
LetsEncryptEmail string
|
AdminUserPassword string
|
||||||
EnableEmail bool
|
DisableSignupWithoutInvite bool
|
||||||
EmailSMTPHost string
|
DisableUserCreateOrg bool
|
||||||
EmailSMTPPort int
|
EnableEmail bool
|
||||||
EmailSMTPUser string
|
EmailSMTPHost string
|
||||||
EmailSMTPPass string
|
EmailSMTPPort int
|
||||||
EmailNoReply string
|
EmailSMTPUser string
|
||||||
InstallGerbil bool
|
EmailSMTPPass string
|
||||||
TraefikBouncerKey string
|
EmailNoReply string
|
||||||
DoCrowdsecInstall bool
|
InstallGerbil bool
|
||||||
Secret string
|
TraefikBouncerKey string
|
||||||
HybridMode bool
|
DoCrowdsecInstall bool
|
||||||
HybridId string
|
Secret string
|
||||||
HybridSecret string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SupportedContainer string
|
|
||||||
|
|
||||||
const (
|
|
||||||
Docker SupportedContainer = "docker"
|
|
||||||
Podman SupportedContainer = "podman"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
reader := bufio.NewReader(os.Stdin)
|
||||||
|
|
||||||
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
|
// check if docker is not installed and the user is root
|
||||||
|
if !isDockerInstalled() {
|
||||||
fmt.Println("Welcome to the Pangolin installer!")
|
if os.Geteuid() != 0 {
|
||||||
fmt.Println("This installer will help you set up Pangolin on your server.")
|
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
||||||
fmt.Println("\nPlease make sure you have the following prerequisites:")
|
os.Exit(1)
|
||||||
fmt.Println("- Open TCP ports 80 and 443 and UDP ports 51820 and 21820 on your VPS and firewall.")
|
|
||||||
fmt.Println("\nLets get started!")
|
|
||||||
|
|
||||||
if os.Geteuid() == 0 { // WE NEED TO BE SUDO TO CHECK THIS
|
|
||||||
for _, p := range []int{80, 443} {
|
|
||||||
if err := checkPortsAvailable(p); err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
|
|
||||||
fmt.Printf("Please close any services on ports 80/443 in order to run the installation smoothly")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(os.Stdin)
|
// check if the user is in the docker group (linux only)
|
||||||
|
if !isUserInDockerGroup() {
|
||||||
|
fmt.Println("You are not in the docker group.")
|
||||||
|
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
var config Config
|
var config Config
|
||||||
|
|
||||||
|
@ -92,26 +84,6 @@ func main() {
|
||||||
config.DoCrowdsecInstall = false
|
config.DoCrowdsecInstall = false
|
||||||
config.Secret = generateRandomSecretKey()
|
config.Secret = generateRandomSecretKey()
|
||||||
|
|
||||||
fmt.Println("\n=== Generating Configuration Files ===")
|
|
||||||
|
|
||||||
// If the secret and id are not generated then generate them
|
|
||||||
if config.HybridMode && (config.HybridId == "" || config.HybridSecret == "") {
|
|
||||||
// fmt.Println("Requesting hybrid credentials from cloud...")
|
|
||||||
credentials, err := requestHybridCredentials()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error requesting hybrid credentials: %v\n", err)
|
|
||||||
fmt.Println("Please obtain credentials manually from the dashboard and run the installer again.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
config.HybridId = credentials.RemoteExitNodeId
|
|
||||||
config.HybridSecret = credentials.Secret
|
|
||||||
fmt.Printf("Your managed credentials have been obtained successfully.\n")
|
|
||||||
fmt.Printf(" ID: %s\n", config.HybridId)
|
|
||||||
fmt.Printf(" Secret: %s\n", config.HybridSecret)
|
|
||||||
fmt.Println("Take these to the Pangolin dashboard https://pangolin.fossorial.io to adopt your node.")
|
|
||||||
readBool(reader, "Have you adopted your node?", true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := createConfigFiles(config); err != nil {
|
if err := createConfigFiles(config); err != nil {
|
||||||
fmt.Printf("Error creating config files: %v\n", err)
|
fmt.Printf("Error creating config files: %v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
@ -119,77 +91,65 @@ func main() {
|
||||||
|
|
||||||
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
moveFile("config/docker-compose.yml", "docker-compose.yml")
|
||||||
|
|
||||||
fmt.Println("\nConfiguration files created successfully!")
|
if !isDockerInstalled() && runtime.GOOS == "linux" {
|
||||||
|
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
||||||
fmt.Println("\n=== Starting installation ===")
|
installDocker()
|
||||||
|
// try to start docker service but ignore errors
|
||||||
if readBool(reader, "Would you like to install and start the containers?", true) {
|
if err := startDockerService(); err != nil {
|
||||||
|
fmt.Println("Error starting Docker service:", err)
|
||||||
config.InstallationContainerType = podmanOrDocker(reader)
|
} else {
|
||||||
|
fmt.Println("Docker service started successfully!")
|
||||||
if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker {
|
|
||||||
if readBool(reader, "Docker is not installed. Would you like to install it?", true) {
|
|
||||||
installDocker()
|
|
||||||
// try to start docker service but ignore errors
|
|
||||||
if err := startDockerService(); err != nil {
|
|
||||||
fmt.Println("Error starting Docker service:", err)
|
|
||||||
} else {
|
|
||||||
fmt.Println("Docker service started successfully!")
|
|
||||||
}
|
|
||||||
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
|
||||||
fmt.Println("Waiting for Docker to start...")
|
|
||||||
for i := 0; i < 5; i++ {
|
|
||||||
if isDockerRunning() {
|
|
||||||
fmt.Println("Docker is running!")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Println("Docker is not running yet, waiting...")
|
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
}
|
|
||||||
if !isDockerRunning() {
|
|
||||||
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("Docker installed successfully!")
|
|
||||||
}
|
}
|
||||||
}
|
// wait 10 seconds for docker to start checking if docker is running every 2 seconds
|
||||||
|
fmt.Println("Waiting for Docker to start...")
|
||||||
if err := pullContainers(config.InstallationContainerType); err != nil {
|
for i := 0; i < 5; i++ {
|
||||||
fmt.Println("Error: ", err)
|
if isDockerRunning() {
|
||||||
return
|
fmt.Println("Docker is running!")
|
||||||
}
|
break
|
||||||
|
}
|
||||||
if err := startContainers(config.InstallationContainerType); err != nil {
|
fmt.Println("Docker is not running yet, waiting...")
|
||||||
fmt.Println("Error: ", err)
|
time.Sleep(2 * time.Second)
|
||||||
return
|
}
|
||||||
|
if !isDockerRunning() {
|
||||||
|
fmt.Println("Docker is still not running after 10 seconds. Please check the installation.")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Println("Docker installed successfully!")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Println("\n=== Starting installation ===")
|
||||||
|
|
||||||
|
if isDockerInstalled() {
|
||||||
|
if readBool(reader, "Would you like to install and start the containers?", true) {
|
||||||
|
if err := pullContainers(); err != nil {
|
||||||
|
fmt.Println("Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := startContainers(); err != nil {
|
||||||
|
fmt.Println("Error: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("Looks like you already installed Pangolin!")
|
fmt.Println("Looks like you already installed, so I am going to do the setup...")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !checkIsCrowdsecInstalledInCompose() && !checkIsPangolinInstalledWithHybrid() {
|
if !checkIsCrowdsecInstalledInCompose() {
|
||||||
fmt.Println("\n=== CrowdSec Install ===")
|
fmt.Println("\n=== CrowdSec Install ===")
|
||||||
// check if crowdsec is installed
|
// check if crowdsec is installed
|
||||||
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
if readBool(reader, "Would you like to install CrowdSec?", false) {
|
||||||
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.")
|
||||||
|
|
||||||
// BUG: crowdsec installation will be skipped if the user chooses to install on the first installation.
|
|
||||||
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
if readBool(reader, "Are you willing to manage CrowdSec?", false) {
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml")
|
traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml", "config/traefik/dynamic_config.yml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("Error reading config: %v\n", err)
|
fmt.Printf("Error reading config: %v\n", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
appConfig, err := ReadAppConfig("config/config.yml")
|
config.DashboardDomain = traefikConfig.DashboardDomain
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("Error reading config: %v\n", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
config.DashboardDomain = appConfig.DashboardURL
|
|
||||||
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
config.LetsEncryptEmail = traefikConfig.LetsEncryptEmail
|
||||||
config.BadgerVersion = traefikConfig.BadgerVersion
|
config.BadgerVersion = traefikConfig.BadgerVersion
|
||||||
|
|
||||||
|
@ -210,97 +170,60 @@ func main() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.HybridMode {
|
fmt.Println("Installation complete!")
|
||||||
// Setup Token Section
|
}
|
||||||
fmt.Println("\n=== Setup Token ===")
|
|
||||||
|
|
||||||
// Check if containers were started during this installation
|
func readString(reader *bufio.Reader, prompt string, defaultValue string) string {
|
||||||
containersStarted := false
|
if defaultValue != "" {
|
||||||
if (isDockerInstalled() && config.InstallationContainerType == Docker) ||
|
fmt.Printf("%s (default: %s): ", prompt, defaultValue)
|
||||||
(isPodmanInstalled() && config.InstallationContainerType == Podman) {
|
} else {
|
||||||
// Try to fetch and display the token if containers are running
|
fmt.Print(prompt + ": ")
|
||||||
containersStarted = true
|
|
||||||
printSetupToken(config.InstallationContainerType, config.DashboardDomain)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If containers weren't started or token wasn't found, show instructions
|
|
||||||
if !containersStarted {
|
|
||||||
showSetupTokenInstructions(config.InstallationContainerType, config.DashboardDomain)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
input, _ := reader.ReadString('\n')
|
||||||
|
input = strings.TrimSpace(input)
|
||||||
|
if input == "" {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println("\nInstallation complete!")
|
func readPassword(prompt string, reader *bufio.Reader) string {
|
||||||
|
if term.IsTerminal(int(syscall.Stdin)) {
|
||||||
if !config.HybridMode && !checkIsPangolinInstalledWithHybrid() {
|
fmt.Print(prompt + ": ")
|
||||||
fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain)
|
// Read password without echo if we're in a terminal
|
||||||
|
password, err := term.ReadPassword(int(syscall.Stdin))
|
||||||
|
fmt.Println() // Add a newline since ReadPassword doesn't add one
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
input := strings.TrimSpace(string(password))
|
||||||
|
if input == "" {
|
||||||
|
return readPassword(prompt, reader)
|
||||||
|
}
|
||||||
|
return input
|
||||||
|
} else {
|
||||||
|
// Fallback to reading from stdin if not in a terminal
|
||||||
|
return readString(reader, prompt, "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func podmanOrDocker(reader *bufio.Reader) SupportedContainer {
|
func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool {
|
||||||
inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
|
defaultStr := "no"
|
||||||
|
if defaultValue {
|
||||||
chosenContainer := Docker
|
defaultStr = "yes"
|
||||||
if strings.EqualFold(inputContainer, "docker") {
|
|
||||||
chosenContainer = Docker
|
|
||||||
} else if strings.EqualFold(inputContainer, "podman") {
|
|
||||||
chosenContainer = Podman
|
|
||||||
} else {
|
|
||||||
fmt.Printf("Unrecognized container type: %s. Valid options are 'docker' or 'podman'.\n", inputContainer)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
input := readString(reader, prompt+" (yes/no)", defaultStr)
|
||||||
|
return strings.ToLower(input) == "yes"
|
||||||
|
}
|
||||||
|
|
||||||
if chosenContainer == Podman {
|
func readInt(reader *bufio.Reader, prompt string, defaultValue int) int {
|
||||||
if !isPodmanInstalled() {
|
input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue))
|
||||||
fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.")
|
if input == "" {
|
||||||
os.Exit(1)
|
return defaultValue
|
||||||
}
|
|
||||||
|
|
||||||
if err := exec.Command("bash", "-c", "cat /etc/sysctl.conf | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil {
|
|
||||||
fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.")
|
|
||||||
fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.")
|
|
||||||
approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p\". Approve?", true)
|
|
||||||
if approved {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
fmt.Println("You need to run the installer as root for such a configuration.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Podman containers are not able to listen on privileged ports. The official recommendation is to
|
|
||||||
// container low-range ports as unprivileged ports.
|
|
||||||
// Linux only.
|
|
||||||
|
|
||||||
if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' >> /etc/sysctl.conf && sysctl -p"); err != nil {
|
|
||||||
fmt.Sprintf("failed to configure unprivileged ports: %v.\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("You need to configure port forwarding or adjust the listening ports before running pangolin.")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fmt.Println("Unprivileged ports have been configured.")
|
|
||||||
}
|
|
||||||
|
|
||||||
} else if chosenContainer == Docker {
|
|
||||||
// check if docker is not installed and the user is root
|
|
||||||
if !isDockerInstalled() {
|
|
||||||
if os.Geteuid() != 0 {
|
|
||||||
fmt.Println("Docker is not installed. Please install Docker manually or run this installer as root.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the user is in the docker group (linux only)
|
|
||||||
if !isUserInDockerGroup() {
|
|
||||||
fmt.Println("You are not in the docker group.")
|
|
||||||
fmt.Println("The installer will not be able to run docker commands without running it as root.")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// This shouldn't happen unless there's a third container runtime.
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
value := defaultValue
|
||||||
return chosenContainer
|
fmt.Sscanf(input, "%d", &value)
|
||||||
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func collectUserInput(reader *bufio.Reader) Config {
|
func collectUserInput(reader *bufio.Reader) Config {
|
||||||
|
@ -308,77 +231,119 @@ func collectUserInput(reader *bufio.Reader) Config {
|
||||||
|
|
||||||
// Basic configuration
|
// Basic configuration
|
||||||
fmt.Println("\n=== Basic Configuration ===")
|
fmt.Println("\n=== Basic Configuration ===")
|
||||||
|
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
||||||
|
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", "pangolin."+config.BaseDomain)
|
||||||
|
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
||||||
|
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
||||||
|
|
||||||
|
// Admin user configuration
|
||||||
|
fmt.Println("\n=== Admin User Configuration ===")
|
||||||
|
config.AdminUserEmail = readString(reader, "Enter admin user email", "admin@"+config.BaseDomain)
|
||||||
for {
|
for {
|
||||||
response := readString(reader, "Do you want to install Pangolin as a cloud-managed (beta) node? (yes/no)", "")
|
pass1 := readPassword("Create admin user password", reader)
|
||||||
if strings.EqualFold(response, "yes") || strings.EqualFold(response, "y") {
|
pass2 := readPassword("Confirm admin user password", reader)
|
||||||
config.HybridMode = true
|
|
||||||
break
|
|
||||||
} else if strings.EqualFold(response, "no") || strings.EqualFold(response, "n") {
|
|
||||||
config.HybridMode = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
fmt.Println("Please answer 'yes' or 'no'")
|
|
||||||
}
|
|
||||||
|
|
||||||
if config.HybridMode {
|
if pass1 != pass2 {
|
||||||
alreadyHaveCreds := readBool(reader, "Do you already have credentials from the dashboard? If not, we will create them later", false)
|
fmt.Println("Passwords do not match")
|
||||||
|
} else {
|
||||||
if alreadyHaveCreds {
|
config.AdminUserPassword = pass1
|
||||||
config.HybridId = readString(reader, "Enter your ID", "")
|
if valid, message := validatePassword(config.AdminUserPassword); valid {
|
||||||
config.HybridSecret = readString(reader, "Enter your secret", "")
|
break
|
||||||
}
|
} else {
|
||||||
|
fmt.Println("Invalid password:", message)
|
||||||
config.DashboardDomain = readString(reader, "The public addressable IP address for this node or a domain pointing to it", "")
|
fmt.Println("Password requirements:")
|
||||||
config.InstallGerbil = true
|
fmt.Println("- At least one uppercase English letter")
|
||||||
} else {
|
fmt.Println("- At least one lowercase English letter")
|
||||||
config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "")
|
fmt.Println("- At least one digit")
|
||||||
|
fmt.Println("- At least one special character")
|
||||||
// Set default dashboard domain after base domain is collected
|
}
|
||||||
defaultDashboardDomain := ""
|
|
||||||
if config.BaseDomain != "" {
|
|
||||||
defaultDashboardDomain = "pangolin." + config.BaseDomain
|
|
||||||
}
|
|
||||||
config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
|
|
||||||
config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "")
|
|
||||||
config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true)
|
|
||||||
|
|
||||||
// Email configuration
|
|
||||||
fmt.Println("\n=== Email Configuration ===")
|
|
||||||
config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false)
|
|
||||||
|
|
||||||
if config.EnableEmail {
|
|
||||||
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
|
||||||
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
|
||||||
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
|
||||||
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword?
|
|
||||||
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if config.BaseDomain == "" {
|
|
||||||
fmt.Println("Error: Domain name is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
if config.LetsEncryptEmail == "" {
|
|
||||||
fmt.Println("Error: Let's Encrypt email is required")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Advanced configuration
|
// Security settings
|
||||||
|
fmt.Println("\n=== Security Settings ===")
|
||||||
|
config.DisableSignupWithoutInvite = readBool(reader, "Disable signup without invite", true)
|
||||||
|
config.DisableUserCreateOrg = readBool(reader, "Disable users from creating organizations", false)
|
||||||
|
|
||||||
fmt.Println("\n=== Advanced Configuration ===")
|
// Email configuration
|
||||||
|
fmt.Println("\n=== Email Configuration ===")
|
||||||
|
config.EnableEmail = readBool(reader, "Enable email functionality", false)
|
||||||
|
|
||||||
config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true)
|
if config.EnableEmail {
|
||||||
|
config.EmailSMTPHost = readString(reader, "Enter SMTP host", "")
|
||||||
|
config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587)
|
||||||
|
config.EmailSMTPUser = readString(reader, "Enter SMTP username", "")
|
||||||
|
config.EmailSMTPPass = readString(reader, "Enter SMTP password", "")
|
||||||
|
config.EmailNoReply = readString(reader, "Enter no-reply email address", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if config.BaseDomain == "" {
|
||||||
|
fmt.Println("Error: Domain name is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
if config.DashboardDomain == "" {
|
if config.DashboardDomain == "" {
|
||||||
fmt.Println("Error: Dashboard Domain name is required")
|
fmt.Println("Error: Dashboard Domain name is required")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if config.LetsEncryptEmail == "" {
|
||||||
|
fmt.Println("Error: Let's Encrypt email is required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if config.AdminUserEmail == "" || config.AdminUserPassword == "" {
|
||||||
|
fmt.Println("Error: Admin user email and password are required")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validatePassword(password string) (bool, string) {
|
||||||
|
if len(password) == 0 {
|
||||||
|
return false, "Password cannot be empty"
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
hasUpper bool
|
||||||
|
hasLower bool
|
||||||
|
hasDigit bool
|
||||||
|
hasSpecial bool
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, char := range password {
|
||||||
|
switch {
|
||||||
|
case unicode.IsUpper(char):
|
||||||
|
hasUpper = true
|
||||||
|
case unicode.IsLower(char):
|
||||||
|
hasLower = true
|
||||||
|
case unicode.IsDigit(char):
|
||||||
|
hasDigit = true
|
||||||
|
case unicode.IsPunct(char) || unicode.IsSymbol(char):
|
||||||
|
hasSpecial = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var missing []string
|
||||||
|
if !hasUpper {
|
||||||
|
missing = append(missing, "an uppercase letter")
|
||||||
|
}
|
||||||
|
if !hasLower {
|
||||||
|
missing = append(missing, "a lowercase letter")
|
||||||
|
}
|
||||||
|
if !hasDigit {
|
||||||
|
missing = append(missing, "a digit")
|
||||||
|
}
|
||||||
|
if !hasSpecial {
|
||||||
|
missing = append(missing, "a special character")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(missing) > 0 {
|
||||||
|
return false, fmt.Sprintf("Password must contain %s", strings.Join(missing, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, ""
|
||||||
|
}
|
||||||
|
|
||||||
func createConfigFiles(config Config) error {
|
func createConfigFiles(config Config) error {
|
||||||
os.MkdirAll("config", 0755)
|
os.MkdirAll("config", 0755)
|
||||||
os.MkdirAll("config/letsencrypt", 0755)
|
os.MkdirAll("config/letsencrypt", 0755)
|
||||||
|
@ -404,11 +369,6 @@ func createConfigFiles(config Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// the hybrid does not need the dynamic config
|
|
||||||
if config.HybridMode && strings.Contains(path, "dynamic_config.yml") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip .DS_Store
|
// skip .DS_Store
|
||||||
if strings.Contains(path, ".DS_Store") {
|
if strings.Contains(path, ".DS_Store") {
|
||||||
return nil
|
return nil
|
||||||
|
@ -453,6 +413,7 @@ func createConfigFiles(config Config) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error walking config files: %v", err)
|
return fmt.Errorf("error walking config files: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -460,6 +421,243 @@ func createConfigFiles(config Config) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func installDocker() error {
|
||||||
|
// Detect Linux distribution
|
||||||
|
cmd := exec.Command("cat", "/etc/os-release")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect Linux distribution: %v", err)
|
||||||
|
}
|
||||||
|
osRelease := string(output)
|
||||||
|
|
||||||
|
// Detect system architecture
|
||||||
|
archCmd := exec.Command("uname", "-m")
|
||||||
|
archOutput, err := archCmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to detect system architecture: %v", err)
|
||||||
|
}
|
||||||
|
arch := strings.TrimSpace(string(archOutput))
|
||||||
|
|
||||||
|
// Map architecture to Docker's architecture naming
|
||||||
|
var dockerArch string
|
||||||
|
switch arch {
|
||||||
|
case "x86_64":
|
||||||
|
dockerArch = "amd64"
|
||||||
|
case "aarch64":
|
||||||
|
dockerArch = "arm64"
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported architecture: %s", arch)
|
||||||
|
}
|
||||||
|
|
||||||
|
var installCmd *exec.Cmd
|
||||||
|
switch {
|
||||||
|
case strings.Contains(osRelease, "ID=ubuntu"):
|
||||||
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||||
|
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
`, dockerArch))
|
||||||
|
case strings.Contains(osRelease, "ID=debian"):
|
||||||
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y apt-transport-https ca-certificates curl software-properties-common &&
|
||||||
|
curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg &&
|
||||||
|
echo "deb [arch=%s signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list &&
|
||||||
|
apt-get update &&
|
||||||
|
apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
`, dockerArch))
|
||||||
|
case strings.Contains(osRelease, "ID=fedora"):
|
||||||
|
// Detect Fedora version to handle DNF 5 changes
|
||||||
|
versionCmd := exec.Command("bash", "-c", "grep VERSION_ID /etc/os-release | cut -d'=' -f2 | tr -d '\"'")
|
||||||
|
versionOutput, err := versionCmd.Output()
|
||||||
|
var fedoraVersion int
|
||||||
|
if err == nil {
|
||||||
|
if v, parseErr := strconv.Atoi(strings.TrimSpace(string(versionOutput))); parseErr == nil {
|
||||||
|
fedoraVersion = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use appropriate DNF syntax based on version
|
||||||
|
var repoCmd string
|
||||||
|
if fedoraVersion >= 41 {
|
||||||
|
// DNF 5 syntax for Fedora 41+
|
||||||
|
repoCmd = "dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||||
|
} else {
|
||||||
|
// DNF 4 syntax for Fedora < 41
|
||||||
|
repoCmd = "dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo"
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd = exec.Command("bash", "-c", fmt.Sprintf(`
|
||||||
|
dnf -y install dnf-plugins-core &&
|
||||||
|
%s &&
|
||||||
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
|
||||||
|
`, repoCmd))
|
||||||
|
case strings.Contains(osRelease, "ID=opensuse") || strings.Contains(osRelease, "ID=\"opensuse-"):
|
||||||
|
installCmd = exec.Command("bash", "-c", `
|
||||||
|
zypper install -y docker docker-compose &&
|
||||||
|
systemctl enable docker
|
||||||
|
`)
|
||||||
|
case strings.Contains(osRelease, "ID=rhel") || strings.Contains(osRelease, "ID=\"rhel"):
|
||||||
|
installCmd = exec.Command("bash", "-c", `
|
||||||
|
dnf remove -y runc &&
|
||||||
|
dnf -y install yum-utils &&
|
||||||
|
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo &&
|
||||||
|
dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin &&
|
||||||
|
systemctl enable docker
|
||||||
|
`)
|
||||||
|
case strings.Contains(osRelease, "ID=amzn"):
|
||||||
|
installCmd = exec.Command("bash", "-c", `
|
||||||
|
yum update -y &&
|
||||||
|
yum install -y docker &&
|
||||||
|
systemctl enable docker &&
|
||||||
|
usermod -a -G docker ec2-user
|
||||||
|
`)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported Linux distribution")
|
||||||
|
}
|
||||||
|
|
||||||
|
installCmd.Stdout = os.Stdout
|
||||||
|
installCmd.Stderr = os.Stderr
|
||||||
|
return installCmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func startDockerService() error {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
cmd := exec.Command("systemctl", "enable", "--now", "docker")
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
} else if runtime.GOOS == "darwin" {
|
||||||
|
// On macOS, Docker is usually started via the Docker Desktop application
|
||||||
|
fmt.Println("Please start Docker Desktop manually on macOS.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("unsupported operating system for starting Docker service")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDockerInstalled() bool {
|
||||||
|
cmd := exec.Command("docker", "--version")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUserInDockerGroup() bool {
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
// Docker group is not applicable on macOS
|
||||||
|
// So we assume that the user can run Docker commands
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if os.Geteuid() == 0 {
|
||||||
|
return true // Root user can run Docker commands anyway
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the current user is in the docker group
|
||||||
|
if dockerGroup, err := user.LookupGroup("docker"); err == nil {
|
||||||
|
if currentUser, err := user.Current(); err == nil {
|
||||||
|
if currentUserGroupIds, err := currentUser.GroupIds(); err == nil {
|
||||||
|
for _, groupId := range currentUserGroupIds {
|
||||||
|
if groupId == dockerGroup.Gid {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Eventually, if any of the checks fail, we assume the user cannot run Docker commands
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isDockerRunning checks if the Docker daemon is running by using the `docker info` command.
|
||||||
|
func isDockerRunning() bool {
|
||||||
|
cmd := exec.Command("docker", "info")
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// executeDockerComposeCommandWithArgs executes the appropriate docker command with arguments supplied
|
||||||
|
func executeDockerComposeCommandWithArgs(args ...string) error {
|
||||||
|
var cmd *exec.Cmd
|
||||||
|
var useNewStyle bool
|
||||||
|
|
||||||
|
if !isDockerInstalled() {
|
||||||
|
return fmt.Errorf("docker is not installed")
|
||||||
|
}
|
||||||
|
|
||||||
|
checkCmd := exec.Command("docker", "compose", "version")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
useNewStyle = true
|
||||||
|
} else {
|
||||||
|
checkCmd = exec.Command("docker-compose", "version")
|
||||||
|
if err := checkCmd.Run(); err == nil {
|
||||||
|
useNewStyle = false
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("neither 'docker compose' nor 'docker-compose' command is available")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if useNewStyle {
|
||||||
|
cmd = exec.Command("docker", append([]string{"compose"}, args...)...)
|
||||||
|
} else {
|
||||||
|
cmd = exec.Command("docker-compose", args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
// pullContainers pulls the containers using the appropriate command.
|
||||||
|
func pullContainers() error {
|
||||||
|
fmt.Println("Pulling the container images...")
|
||||||
|
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "pull", "--policy", "always"); err != nil {
|
||||||
|
return fmt.Errorf("failed to pull the containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// startContainers starts the containers using the appropriate command.
|
||||||
|
func startContainers() error {
|
||||||
|
fmt.Println("Starting containers...")
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "up", "-d", "--force-recreate"); err != nil {
|
||||||
|
return fmt.Errorf("failed to start containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// stopContainers stops the containers using the appropriate command.
|
||||||
|
func stopContainers() error {
|
||||||
|
fmt.Println("Stopping containers...")
|
||||||
|
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "down"); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop containers: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// restartContainer restarts a specific container using the appropriate command.
|
||||||
|
func restartContainer(container string) error {
|
||||||
|
fmt.Println("Restarting containers...")
|
||||||
|
|
||||||
|
if err := executeDockerComposeCommandWithArgs("-f", "docker-compose.yml", "restart", container); err != nil {
|
||||||
|
return fmt.Errorf("failed to stop the container \"%s\": %v", container, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func copyFile(src, dst string) error {
|
func copyFile(src, dst string) error {
|
||||||
source, err := os.Open(src)
|
source, err := os.Open(src)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -485,89 +683,32 @@ func moveFile(src, dst string) error {
|
||||||
return os.Remove(src)
|
return os.Remove(src)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printSetupToken(containerType SupportedContainer, dashboardDomain string) {
|
func waitForContainer(containerName string) error {
|
||||||
fmt.Println("Waiting for Pangolin to generate setup token...")
|
maxAttempts := 30
|
||||||
|
retryInterval := time.Second * 2
|
||||||
|
|
||||||
// Wait for Pangolin to be healthy
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
if err := waitForContainer("pangolin", containerType); err != nil {
|
// Check if container is running
|
||||||
fmt.Println("Warning: Pangolin container did not become healthy in time.")
|
cmd := exec.Command("docker", "container", "inspect", "-f", "{{.State.Running}}", containerName)
|
||||||
return
|
var out bytes.Buffer
|
||||||
}
|
cmd.Stdout = &out
|
||||||
|
|
||||||
// Give a moment for the setup token to be generated
|
if err := cmd.Run(); err != nil {
|
||||||
time.Sleep(2 * time.Second)
|
// If the container doesn't exist or there's another error, wait and retry
|
||||||
|
time.Sleep(retryInterval)
|
||||||
// Fetch logs
|
continue
|
||||||
var cmd *exec.Cmd
|
|
||||||
if containerType == Docker {
|
|
||||||
cmd = exec.Command("docker", "logs", "pangolin")
|
|
||||||
} else {
|
|
||||||
cmd = exec.Command("podman", "logs", "pangolin")
|
|
||||||
}
|
|
||||||
output, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Warning: Could not fetch Pangolin logs to find setup token.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse for setup token
|
|
||||||
lines := strings.Split(string(output), "\n")
|
|
||||||
for i, line := range lines {
|
|
||||||
if strings.Contains(line, "=== SETUP TOKEN GENERATED ===") || strings.Contains(line, "=== SETUP TOKEN EXISTS ===") {
|
|
||||||
// Look for "Token: ..." in the next few lines
|
|
||||||
for j := i + 1; j < i+5 && j < len(lines); j++ {
|
|
||||||
trimmedLine := strings.TrimSpace(lines[j])
|
|
||||||
if strings.Contains(trimmedLine, "Token:") {
|
|
||||||
// Extract token after "Token:"
|
|
||||||
tokenStart := strings.Index(trimmedLine, "Token:")
|
|
||||||
if tokenStart != -1 {
|
|
||||||
token := strings.TrimSpace(trimmedLine[tokenStart+6:])
|
|
||||||
fmt.Printf("Setup token: %s\n", token)
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("This token is required to register the first admin account in the web UI at:")
|
|
||||||
fmt.Printf("https://%s/auth/initial-setup\n", dashboardDomain)
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("Save this token securely. It will be invalid after the first admin is created.")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
fmt.Println("Warning: Could not find a setup token in Pangolin logs.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomain string) {
|
isRunning := strings.TrimSpace(out.String()) == "true"
|
||||||
fmt.Println("\n=== Setup Token Instructions ===")
|
if isRunning {
|
||||||
fmt.Println("To get your setup token, you need to:")
|
return nil
|
||||||
fmt.Println("")
|
}
|
||||||
fmt.Println("1. Start the containers:")
|
|
||||||
if containerType == Docker {
|
// Container exists but isn't running yet, wait and retry
|
||||||
fmt.Println(" docker-compose up -d")
|
time.Sleep(retryInterval)
|
||||||
} else {
|
|
||||||
fmt.Println(" podman-compose up -d")
|
|
||||||
}
|
}
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("2. Wait for the Pangolin container to start and generate the token")
|
return fmt.Errorf("container %s did not start within %v seconds", containerName, maxAttempts*int(retryInterval.Seconds()))
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("3. Check the container logs for the setup token:")
|
|
||||||
if containerType == Docker {
|
|
||||||
fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
|
||||||
} else {
|
|
||||||
fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'")
|
|
||||||
}
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("4. Look for output like:")
|
|
||||||
fmt.Println(" === SETUP TOKEN GENERATED ===")
|
|
||||||
fmt.Println(" Token: [your-token-here]")
|
|
||||||
fmt.Println(" Use this token on the initial setup page")
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("5. Use the token to complete initial setup at:")
|
|
||||||
fmt.Printf(" https://%s/auth/initial-setup\n", dashboardDomain)
|
|
||||||
fmt.Println("")
|
|
||||||
fmt.Println("The setup token is required to register the first admin account.")
|
|
||||||
fmt.Println("Save it securely - it will be invalid after the first admin is created.")
|
|
||||||
fmt.Println("================================")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateRandomSecretKey() string {
|
func generateRandomSecretKey() string {
|
||||||
|
@ -583,45 +724,3 @@ func generateRandomSecretKey() string {
|
||||||
}
|
}
|
||||||
return string(b)
|
return string(b)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run external commands with stdio/stderr attached.
|
|
||||||
func run(name string, args ...string) error {
|
|
||||||
cmd := exec.Command(name, args...)
|
|
||||||
cmd.Stdout = os.Stdout
|
|
||||||
cmd.Stderr = os.Stderr
|
|
||||||
return cmd.Run()
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkPortsAvailable(port int) error {
|
|
||||||
addr := fmt.Sprintf(":%d", port)
|
|
||||||
ln, err := net.Listen("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"ERROR: port %d is occupied or cannot be bound: %w\n\n",
|
|
||||||
port, err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if closeErr := ln.Close(); closeErr != nil {
|
|
||||||
fmt.Fprintf(os.Stderr,
|
|
||||||
"WARNING: failed to close test listener on port %d: %v\n",
|
|
||||||
port, closeErr,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkIsPangolinInstalledWithHybrid() bool {
|
|
||||||
// Check if config/config.yml exists and contains hybrid section
|
|
||||||
if _, err := os.Stat("config/config.yml"); err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read config file to check for hybrid section
|
|
||||||
content, err := os.ReadFile("config/config.yml")
|
|
||||||
if err != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for hybrid section
|
|
||||||
return bytes.Contains(content, []byte("managed:"))
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FRONTEND_SECRET_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e"
|
|
||||||
// CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
CLOUD_API_URL = "https://pangolin.fossorial.io/api/v1/remote-exit-node/quick-start"
|
|
||||||
)
|
|
||||||
|
|
||||||
// HybridCredentials represents the response from the cloud API
|
|
||||||
type HybridCredentials struct {
|
|
||||||
RemoteExitNodeId string `json:"remoteExitNodeId"`
|
|
||||||
Secret string `json:"secret"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIResponse represents the full response structure from the cloud API
|
|
||||||
type APIResponse struct {
|
|
||||||
Data HybridCredentials `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// RequestPayload represents the request body structure
|
|
||||||
type RequestPayload struct {
|
|
||||||
Token string `json:"token"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateValidationToken() string {
|
|
||||||
timestamp := time.Now().UnixMilli()
|
|
||||||
data := fmt.Sprintf("%s|%d", FRONTEND_SECRET_KEY, timestamp)
|
|
||||||
obfuscated := make([]byte, len(data))
|
|
||||||
for i, char := range []byte(data) {
|
|
||||||
obfuscated[i] = char + 5
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(obfuscated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// requestHybridCredentials makes an HTTP POST request to the cloud API
|
|
||||||
// to get hybrid credentials (ID and secret)
|
|
||||||
func requestHybridCredentials() (*HybridCredentials, error) {
|
|
||||||
// Generate validation token
|
|
||||||
token := generateValidationToken()
|
|
||||||
|
|
||||||
// Create request payload
|
|
||||||
payload := RequestPayload{
|
|
||||||
Token: token,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Marshal payload to JSON
|
|
||||||
jsonData, err := json.Marshal(payload)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to marshal request payload: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create HTTP request
|
|
||||||
req, err := http.NewRequest("POST", CLOUD_API_URL, bytes.NewBuffer(jsonData))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set headers
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
req.Header.Set("X-CSRF-Token", "x-csrf-protection")
|
|
||||||
|
|
||||||
// Create HTTP client with timeout
|
|
||||||
client := &http.Client{
|
|
||||||
Timeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make the request
|
|
||||||
resp, err := client.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to make HTTP request: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Check response status
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return nil, fmt.Errorf("API request failed with status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read response body for debugging
|
|
||||||
body, err := io.ReadAll(resp.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to read response body: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Print the raw JSON response for debugging
|
|
||||||
// fmt.Printf("Raw JSON response: %s\n", string(body))
|
|
||||||
|
|
||||||
// Parse response
|
|
||||||
var apiResponse APIResponse
|
|
||||||
if err := json.Unmarshal(body, &apiResponse); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to decode API response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate response data
|
|
||||||
if apiResponse.Data.RemoteExitNodeId == "" || apiResponse.Data.Secret == "" {
|
|
||||||
return nil, fmt.Errorf("invalid response: missing remoteExitNodeId or secret")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &apiResponse.Data, nil
|
|
||||||
}
|
|
287
internationalization/de.md
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
## Authentication Site
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
|
||||||
|
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Bereitgestellt von [Pangolin](https://github.com/fosrl/pangolin) | |
|
||||||
|
| Authentication Required | Authentifizierung erforderlich | |
|
||||||
|
| Choose your preferred method to access {resource} | Wählen Sie Ihre bevorzugte Methode, um auf {resource} zuzugreifen | |
|
||||||
|
| PIN | PIN | |
|
||||||
|
| User | Benutzer | |
|
||||||
|
| 6-digit PIN Code | 6-stelliger PIN-Code | pin login |
|
||||||
|
| Login in with PIN | Mit PIN anmelden | pin login |
|
||||||
|
| Email | E-Mail | user login |
|
||||||
|
| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | user login |
|
||||||
|
| Password | Passwort | user login |
|
||||||
|
| Enter your password | Geben Sie Ihr Passwort ein | user login |
|
||||||
|
| Forgot your password? | Passwort vergessen? | user login |
|
||||||
|
| Log in | Anmelden | user login |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Login site
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| --------------------- | ---------------------------------- | ----------- |
|
||||||
|
| Welcome to Pangolin | Willkommen bei Pangolin | |
|
||||||
|
| Log in to get started | Melden Sie sich an, um zu beginnen | |
|
||||||
|
| Email | E-Mail | |
|
||||||
|
| Enter your email | Geben Sie Ihre E-Mail-Adresse ein | placeholder |
|
||||||
|
| Password | Passwort | |
|
||||||
|
| Enter your password | Geben Sie Ihr Passwort ein | placeholder |
|
||||||
|
| Forgot your password? | Passwort vergessen? | |
|
||||||
|
| Log in | Anmelden | |
|
||||||
|
|
||||||
|
# Ogranization site after successful login
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ----------------------------------------- | -------------------------------------------- | ----- |
|
||||||
|
| Welcome to Pangolin | Willkommen bei Pangolin | |
|
||||||
|
| You're a member of {number} organization. | Sie sind Mitglied von {number} Organisation. | |
|
||||||
|
|
||||||
|
## Shared Header, Navbar and Footer
|
||||||
|
##### Header
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------- | ------------------- | ----- |
|
||||||
|
| Documentation | Dokumentation | |
|
||||||
|
| Support | Support | |
|
||||||
|
| Organization {name} | Organisation {name} | |
|
||||||
|
##### Organization selector
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ---------------- | ----------------- | ----- |
|
||||||
|
| Search… | Suchen… | |
|
||||||
|
| Create | Erstellen | |
|
||||||
|
| New Organization | Neue Organisation | |
|
||||||
|
| Organizations | Organisationen | |
|
||||||
|
|
||||||
|
##### Navbar
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| --------------- | ----------------- | ----- |
|
||||||
|
| Sites | Websites | |
|
||||||
|
| Resources | Ressourcen | |
|
||||||
|
| User & Roles | Benutzer & Rollen | |
|
||||||
|
| Shareable Links | Teilbare Links | |
|
||||||
|
| General | Allgemein | |
|
||||||
|
##### Footer
|
||||||
|
| EN | DE | |
|
||||||
|
| ------------------------- | --------------------------- | ------------------- |
|
||||||
|
| Page {number} of {number} | Seite {number} von {number} | |
|
||||||
|
| Rows per page | Zeilen pro Seite | |
|
||||||
|
| Pangolin | Pangolin | unten auf der Seite |
|
||||||
|
| Built by Fossorial | Erstellt von Fossorial | unten auf der Seite |
|
||||||
|
| Open Source | Open Source | unten auf der Seite |
|
||||||
|
| Documentation | Dokumentation | unten auf der Seite |
|
||||||
|
| {version} | {version} | unten auf der Seite |
|
||||||
|
|
||||||
|
## Main “Sites”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Newt (Recommended) | Newt (empfohlen) | |
|
||||||
|
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Für das beste Benutzererlebnis verwenden Sie Newt. Es nutzt WireGuard im Hintergrund und ermöglicht es Ihnen, auf Ihre privaten Ressourcen über ihre LAN-Adresse in Ihrem privaten Netzwerk direkt aus dem Pangolin-Dashboard zuzugreifen. | |
|
||||||
|
| Runs in Docker | Läuft in Docker | |
|
||||||
|
| Runs in shell on macOS, Linux, and Windows | Läuft in der Shell auf macOS, Linux und Windows | |
|
||||||
|
| Install Newt | Newt installieren | |
|
||||||
|
| Basic WireGuard<br> | Verwenden Sie einen beliebigen WireGuard-Client, um eine Verbindung herzustellen. Sie müssen auf Ihre internen Ressourcen über die Peer-IP-Adresse zugreifen. | |
|
||||||
|
| Compatible with all WireGuard clients<br> | Kompatibel mit allen WireGuard-Clients<br> | |
|
||||||
|
| Manual configuration required | Manuelle Konfiguration erforderlich<br> | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
|
||||||
|
| Manage Sites | Seiten verwalten | |
|
||||||
|
| Allow connectivity to your network through secure tunnels | Ermöglichen Sie die Verbindung zu Ihrem Netzwerk über ein sicheren Tunnel | |
|
||||||
|
| Search sites | Seiten suchen | placeholder |
|
||||||
|
| Add Site | Seite hinzufügen | |
|
||||||
|
| Name | Name | table header |
|
||||||
|
| Online | Status | table header |
|
||||||
|
| Site | Seite | table header |
|
||||||
|
| Data In | Eingehende Daten | table header |
|
||||||
|
| Data Out | Ausgehende Daten | table header |
|
||||||
|
| Connection Type | Verbindungstyp | table header |
|
||||||
|
| Online | Online | site state |
|
||||||
|
| Offline | Offline | site state |
|
||||||
|
| Edit → | Bearbeiten → | |
|
||||||
|
| View settings | Einstellungen anzeigen | Popup after clicking “…” on site |
|
||||||
|
| Delete | Löschen | Popup after clicking “…” on site |
|
||||||
|
##### Add Site Popup
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
|
||||||
|
| Create Site | Seite erstellen | |
|
||||||
|
| Create a new site to start connection for this site | Erstellen Sie eine neue Seite, um die Verbindung zu starten | |
|
||||||
|
| Name | Name | |
|
||||||
|
| Site name | Seiten-Name | placeholder |
|
||||||
|
| This is the name that will be displayed for this site. | So wird Ihre Seite angezeigt | desc |
|
||||||
|
| Method | Methode | |
|
||||||
|
| Local | Lokal | |
|
||||||
|
| Newt | Newt | |
|
||||||
|
| WireGuard | WireGuard | |
|
||||||
|
| This is how you will expose connections. | So werden Verbindungen freigegeben. | |
|
||||||
|
| You will only be able to see the configuration once. | Diese Konfiguration können Sie nur einmal sehen. | |
|
||||||
|
| Learn how to install Newt on your system | Erfahren Sie, wie Sie Newt auf Ihrem System installieren | |
|
||||||
|
| I have copied the config | Ich habe die Konfiguration kopiert | |
|
||||||
|
| Create Site | Website erstellen | |
|
||||||
|
| Close | Schließen | |
|
||||||
|
|
||||||
|
## Main “Resources”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Resources | Ressourcen | |
|
||||||
|
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | |
|
||||||
|
| Secure connectivity with WireGuard encryption | Sichere Verbindung mit WireGuard-Verschlüsselung | |
|
||||||
|
| Configure multiple authentication methods | Konfigurieren Sie mehrere Authentifizierungsmethoden | |
|
||||||
|
| User and role-based access control | Benutzer- und rollenbasierte Zugriffskontrolle | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
|
||||||
|
| Manage Resources | Ressourcen verwalten | |
|
||||||
|
| Create secure proxies to your private applications | Erstellen Sie sichere Proxys für Ihre privaten Anwendungen | |
|
||||||
|
| Search resources | Ressourcen durchsuchen | placeholder |
|
||||||
|
| Name | Name | |
|
||||||
|
| Site | Website | |
|
||||||
|
| Full URL | Vollständige URL | |
|
||||||
|
| Authentication | Authentifizierung | |
|
||||||
|
| Not Protected | Nicht geschützt | authentication state |
|
||||||
|
| Protected | Geschützt | authentication state |
|
||||||
|
| Edit → | Bearbeiten → | |
|
||||||
|
| Add Resource | Ressource hinzufügen | |
|
||||||
|
##### Add Resource Popup
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
|
||||||
|
| Create Resource | Ressource erstellen | |
|
||||||
|
| Create a new resource to proxy request to your app | Erstellen Sie eine neue Ressource, um Anfragen an Ihre App zu proxen | |
|
||||||
|
| Name | Name | |
|
||||||
|
| My Resource | Neue Ressource | name placeholder |
|
||||||
|
| This is the name that will be displayed for this resource. | Dies ist der Name, der für diese Ressource angezeigt wird | |
|
||||||
|
| Subdomain | Subdomain | |
|
||||||
|
| Enter subdomain | Subdomain eingeben | |
|
||||||
|
| This is the fully qualified domain name that will be used to access the resource. | Dies ist der vollständige Domainname, der für den Zugriff auf die Ressource verwendet wird. | |
|
||||||
|
| Site | Website | |
|
||||||
|
| Search site… | Website suchen… | Site selector popup |
|
||||||
|
| This is the site that will be used in the dashboard. | Dies ist die Website, die im Dashboard verwendet wird. | |
|
||||||
|
| Create Resource | Ressource erstellen | |
|
||||||
|
| Close | Schließen | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “User & Roles”
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
|
||||||
|
| Manage User & Roles | Benutzer & Rollen verwalten | |
|
||||||
|
| Invite users and add them to roles to manage access to your organization | Laden Sie Benutzer ein und weisen Sie ihnen Rollen zu, um den Zugriff auf Ihre Organisation zu verwalten | |
|
||||||
|
| Users | Benutzer | sidebar item |
|
||||||
|
| Roles | Rollen | sidebar item |
|
||||||
|
| **User tab** | | |
|
||||||
|
| Search users | Benutzer suchen | placeholder |
|
||||||
|
| Invite User | Benutzer einladen | addbutton |
|
||||||
|
| Email | E-Mail | table header |
|
||||||
|
| Status | Status | table header |
|
||||||
|
| Role | Rolle | table header |
|
||||||
|
| Confirmed | Bestätigt | account status |
|
||||||
|
| Not confirmed (?) | Nicht bestätigt (?) | unknown for me account status |
|
||||||
|
| Owner | Besitzer | role |
|
||||||
|
| Admin | Administrator | role |
|
||||||
|
| Member | Mitglied | role |
|
||||||
|
| **Roles Tab** | | |
|
||||||
|
| Search roles | Rollen suchen | placeholder |
|
||||||
|
| Add Role | Rolle hinzufügen | addbutton |
|
||||||
|
| Name | Name | table header |
|
||||||
|
| Description | Beschreibung | table header |
|
||||||
|
| Admin | Administrator | role |
|
||||||
|
| Member | Mitglied | role |
|
||||||
|
| Admin role with the most permissions | Administratorrolle mit den meisten Berechtigungen | admin role desc |
|
||||||
|
| Members can only view resources | Mitglieder können nur Ressourcen anzeigen | member role desc |
|
||||||
|
|
||||||
|
##### Invite User popup
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ----------------- | ------------------------------------------------------- | ----------- |
|
||||||
|
| Invite User | Geben Sie neuen Benutzern Zugriff auf Ihre Organisation | |
|
||||||
|
| Email | E-Mail | |
|
||||||
|
| Enter an email | E-Mail eingeben | placeholder |
|
||||||
|
| Role | Rolle | |
|
||||||
|
| Select role | Rolle auswählen | placeholder |
|
||||||
|
| Gültig für | Gültig bis | |
|
||||||
|
| 1 day | Tag | |
|
||||||
|
| 2 days | 2 Tage | |
|
||||||
|
| 3 days | 3 Tage | |
|
||||||
|
| 4 days | 4 Tage | |
|
||||||
|
| 5 days | 5 Tage | |
|
||||||
|
| 6 days | 6 Tage | |
|
||||||
|
| 7 days | 7 Tage | |
|
||||||
|
| Create Invitation | Einladung erstellen | |
|
||||||
|
| Close | Schließen | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “Shareable Links”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Shareable Links | Teilbare Links | |
|
||||||
|
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Erstellen Sie teilbare Links zu Ihren Ressourcen. Links bieten temporären oder unbegrenzten Zugriff auf Ihre Ressource. Sie können die Gültigkeitsdauer des Links beim Erstellen konfigurieren. | |
|
||||||
|
| Easy to create and share | Einfach zu erstellen und zu teilen | |
|
||||||
|
| Configurable expiration duration | Konfigurierbare Gültigkeitsdauer | |
|
||||||
|
| Secure and revocable | Sicher und widerrufbar | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
|
||||||
|
| Manage Shareable Links | Teilbare Links verwalten | |
|
||||||
|
| Create shareable links to grant temporary or permanent access to your resources | Erstellen Sie teilbare Links, um temporären oder permanenten Zugriff auf Ihre Ressourcen zu gewähren | |
|
||||||
|
| Search links | Links suchen | placeholder |
|
||||||
|
| Create Share Link | Neuen Link erstellen | addbutton |
|
||||||
|
| Resource | Ressource | table header |
|
||||||
|
| Title | Titel | table header |
|
||||||
|
| Created | Erstellt | table header |
|
||||||
|
| Expires | Gültig bis | table header |
|
||||||
|
| No links. Create one to get started. | Keine Links. Erstellen Sie einen, um zu beginnen. | table placeholder |
|
||||||
|
|
||||||
|
##### Create Shareable Link popup
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
|
||||||
|
| Create Shareable Link | Teilbaren Link erstellen | |
|
||||||
|
| Anyone with this link can access the resource | Jeder mit diesem Link kann auf die Ressource zugreifen | |
|
||||||
|
| Resource | Ressource | |
|
||||||
|
| Select resource | Ressource auswählen | |
|
||||||
|
| Search resources… | Ressourcen suchen… | resource selector popup |
|
||||||
|
| Title (optional) | Titel (optional) | |
|
||||||
|
| Enter title | Titel eingeben | placeholder |
|
||||||
|
| Expire in | Gültig bis | |
|
||||||
|
| Minutes | Minuten | |
|
||||||
|
| Hours | Stunden | |
|
||||||
|
| Days | Tage | |
|
||||||
|
| Months | Monate | |
|
||||||
|
| Years | Jahre | |
|
||||||
|
| Never expire | Nie ablaufen | |
|
||||||
|
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Die Gültigkeitsdauer bestimmt, wie lange der Link nutzbar ist und Zugriff auf die Ressource bietet. Nach Ablauf dieser Zeit funktioniert der Link nicht mehr, und Benutzer, die diesen Link verwendet haben, verlieren den Zugriff auf die Ressource. | |
|
||||||
|
| Create Link | Link erstellen | |
|
||||||
|
| Close | Schließen | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “General”
|
||||||
|
|
||||||
|
| EN | DE | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
|
||||||
|
| General | Allgemein | |
|
||||||
|
| Configure your organization’s general settings | Konfigurieren Sie die allgemeinen Einstellungen Ihrer Organisation | |
|
||||||
|
| General | Allgemein | sidebar item |
|
||||||
|
| Organization Settings | Organisationseinstellungen | |
|
||||||
|
| Manage your organization details and configuration | Verwalten Sie die Details und Konfiguration Ihrer Organisation | |
|
||||||
|
| Name | Name | |
|
||||||
|
| This is the display name of the org | Dies ist der Anzeigename Ihrer Organisation | |
|
||||||
|
| Save Settings | Einstellungen speichern | |
|
||||||
|
| Danger Zone | Gefahrenzone | |
|
||||||
|
| Once you delete this org, there is no going back. Please be certain. | Wenn Sie diese Organisation löschen, gibt es kein Zurück. Bitte seien Sie sicher. | |
|
||||||
|
| Delete Organization Data | Organisationsdaten löschen | |
|
291
internationalization/es.md
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
## Authentication Site
|
||||||
|
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
|
||||||
|
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Desarrollado por [Pangolin](https://github.com/fosrl/pangolin) | |
|
||||||
|
| Authentication Required | Se requiere autenticación | |
|
||||||
|
| Choose your preferred method to access {resource} | Elije tu método requerido para acceder a {resource} | |
|
||||||
|
| PIN | PIN | |
|
||||||
|
| User | Usuario | |
|
||||||
|
| 6-digit PIN Code | Código PIN de 6 dígitos | pin login |
|
||||||
|
| Login in with PIN | Registrate con PIN | pin login |
|
||||||
|
| Email | Email | user login |
|
||||||
|
| Enter your email | Introduce tu email | user login |
|
||||||
|
| Password | Contraseña | user login |
|
||||||
|
| Enter your password | Introduce tu contraseña | user login |
|
||||||
|
| Forgot your password? | ¿Olvidaste tu contraseña? | user login |
|
||||||
|
| Log in | Iniciar sesión | user login |
|
||||||
|
|
||||||
|
|
||||||
|
## Login site
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| --------------------- | ---------------------------------- | ----------- |
|
||||||
|
| Welcome to Pangolin | Binvenido a Pangolin | |
|
||||||
|
| Log in to get started | Registrate para comenzar | |
|
||||||
|
| Email | Email | |
|
||||||
|
| Enter your email | Introduce tu email | placeholder |
|
||||||
|
| Password | Contraseña | |
|
||||||
|
| Enter your password | Introduce tu contraseña | placeholder |
|
||||||
|
| Forgot your password? | ¿Olvidaste tu contraseña? | |
|
||||||
|
| Log in | Iniciar sesión | |
|
||||||
|
|
||||||
|
# Ogranization site after successful login
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ----------------------------------------- | -------------------------------------------- | ----- |
|
||||||
|
| Welcome to Pangolin | Binvenido a Pangolin | |
|
||||||
|
| You're a member of {number} organization. | Eres miembro de la organización {number}. | |
|
||||||
|
|
||||||
|
## Shared Header, Navbar and Footer
|
||||||
|
##### Header
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------- | ------------------- | ----- |
|
||||||
|
| Documentation | Documentación | |
|
||||||
|
| Support | Soporte | |
|
||||||
|
| Organization {name} | Organización {name} | |
|
||||||
|
##### Organization selector
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ---------------- | ----------------- | ----- |
|
||||||
|
| Search… | Buscar… | |
|
||||||
|
| Create | Crear | |
|
||||||
|
| New Organization | Nueva Organización| |
|
||||||
|
| Organizations | Organizaciones | |
|
||||||
|
|
||||||
|
##### Navbar
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| --------------- | -----------------------| ----- |
|
||||||
|
| Sites | Sitios | |
|
||||||
|
| Resources | Recursos | |
|
||||||
|
| User & Roles | Usuarios y roles | |
|
||||||
|
| Shareable Links | Enlaces para compartir | |
|
||||||
|
| General | General | |
|
||||||
|
|
||||||
|
##### Footer
|
||||||
|
| EN | ES | |
|
||||||
|
| ------------------------- | --------------------------- | -------|
|
||||||
|
| Page {number} of {number} | Página {number} de {number} | footer |
|
||||||
|
| Rows per page | Filas por página | footer |
|
||||||
|
| Pangolin | Pangolin | footer |
|
||||||
|
| Built by Fossorial | Construido por Fossorial | footer |
|
||||||
|
| Open Source | Código abierto | footer |
|
||||||
|
| Documentation | Documentación | footer |
|
||||||
|
| {version} | {version} | footer |
|
||||||
|
|
||||||
|
## Main “Sites”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Newt (Recommended) | Newt (Recomendado) | |
|
||||||
|
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Para obtener la mejor experiencia de usuario, utiliza Newt. Utiliza WireGuard internamente y te permite abordar tus recursos privados mediante tu dirección LAN en tu red privada desde el panel de Pangolin. | |
|
||||||
|
| Runs in Docker | Se ejecuta en Docker | |
|
||||||
|
| Runs in shell on macOS, Linux, and Windows | Se ejecuta en shell en macOS, Linux y Windows | |
|
||||||
|
| Install Newt | Instalar Newt | |
|
||||||
|
| Basic WireGuard<br> | WireGuard básico<br> | |
|
||||||
|
| Compatible with all WireGuard clients<br> | Compatible con todos los clientes WireGuard<br> | |
|
||||||
|
| Manual configuration required | Se requiere configuración manual | |
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| --------------------------------------------------------- | ------------------------------------------------------------ | -------------------------------- |
|
||||||
|
| Manage Sites | Administrar sitios | |
|
||||||
|
| Allow connectivity to your network through secure tunnels | Permitir la conectividad a tu red a través de túneles seguros| |
|
||||||
|
| Search sites | Buscar sitios | placeholder |
|
||||||
|
| Add Site | Agregar sitio | |
|
||||||
|
| Name | Nombre | table header |
|
||||||
|
| Online | Conectado | table header |
|
||||||
|
| Site | Sitio | table header |
|
||||||
|
| Data In | Datos en | table header |
|
||||||
|
| Data Out | Datos de salida | table header |
|
||||||
|
| Connection Type | Tipo de conexión | table header |
|
||||||
|
| Online | Conectado | site state |
|
||||||
|
| Offline | Desconectado | site state |
|
||||||
|
| Edit → | Editar → | |
|
||||||
|
| View settings | Ver configuración | Popup after clicking “…” on site |
|
||||||
|
| Delete | Borrar | Popup after clicking “…” on site |
|
||||||
|
|
||||||
|
##### Add Site Popup
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------ | ----------------------------------------------------------- | ----------- |
|
||||||
|
| Create Site | Crear sitio | |
|
||||||
|
| Create a new site to start connection for this site | Crear un nuevo sitio para iniciar la conexión para este sitio | |
|
||||||
|
| Name | Nombre | |
|
||||||
|
| Site name | Nombre del sitio | placeholder |
|
||||||
|
| This is the name that will be displayed for this site. | Este es el nombre que se mostrará para este sitio. | desc |
|
||||||
|
| Method | Método | |
|
||||||
|
| Local | Local | |
|
||||||
|
| Newt | Newt | |
|
||||||
|
| WireGuard | WireGuard | |
|
||||||
|
| This is how you will expose connections. | Así es como expondrás las conexiones. | |
|
||||||
|
| You will only be able to see the configuration once. | Solo podrás ver la configuración una vez. | |
|
||||||
|
| Learn how to install Newt on your system | Aprende a instalar Newt en tu sistema | |
|
||||||
|
| I have copied the config | He copiado la configuración | |
|
||||||
|
| Create Site | Crear sitio | |
|
||||||
|
| Close | Cerrar | |
|
||||||
|
|
||||||
|
## Main “Resources”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Resources | Recursos | |
|
||||||
|
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. |Los recursos son servidores proxy para aplicaciones que se ejecutan en su red privada. Cree un recurso para cada aplicación HTTP o HTTPS en su red privada. Cada recurso debe estar conectado a un sitio web para proporcionar una conexión privada y segura a través del túnel cifrado WireGuard. | |
|
||||||
|
| Secure connectivity with WireGuard encryption | Conectividad segura con encriptación WireGuard | |
|
||||||
|
| Configure multiple authentication methods | Configura múltiples métodos de autenticación | |
|
||||||
|
| User and role-based access control | Control de acceso basado en usuarios y roles | |
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| -------------------------------------------------- | ---------------------------------------------------------- | -------------------- |
|
||||||
|
| Manage Resources | Administrar recursos | |
|
||||||
|
| Create secure proxies to your private applications | Crea servidores proxy seguros para tus aplicaciones privadas | |
|
||||||
|
| Search resources | Buscar recursos | placeholder |
|
||||||
|
| Name | Nombre | |
|
||||||
|
| Site | Sitio | |
|
||||||
|
| Full URL | URL completa | |
|
||||||
|
| Authentication | Autenticación | |
|
||||||
|
| Not Protected | No protegido | authentication state |
|
||||||
|
| Protected | Protegido | authentication state |
|
||||||
|
| Edit → | Editar → | |
|
||||||
|
| Add Resource | Agregar recurso | |
|
||||||
|
|
||||||
|
##### Add Resource Popup
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------- |
|
||||||
|
| Create Resource | Crear recurso | |
|
||||||
|
| Create a new resource to proxy request to your app | Crea un nuevo recurso para enviar solicitudes a tu aplicación | |
|
||||||
|
| Name | Nombre | |
|
||||||
|
| My Resource | Mi recurso | name placeholder |
|
||||||
|
| This is the name that will be displayed for this resource. | Este es el nombre que se mostrará para este recurso. | |
|
||||||
|
| Subdomain | Subdominio | |
|
||||||
|
| Enter subdomain | Ingresar subdominio | |
|
||||||
|
| This is the fully qualified domain name that will be used to access the resource. | Este es el nombre de dominio completo que se utilizará para acceder al recurso. | |
|
||||||
|
| Site | Sitio | |
|
||||||
|
| Search site… | Buscar sitio… | Site selector popup |
|
||||||
|
| This is the site that will be used in the dashboard. | Este es el sitio que se utilizará en el panel de control. | |
|
||||||
|
| Create Resource | Crear recurso | |
|
||||||
|
| Close | Cerrar | |
|
||||||
|
|
||||||
|
## Main “User & Roles”
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------------- |
|
||||||
|
| Manage User & Roles | Administrar usuarios y roles | |
|
||||||
|
| Invite users and add them to roles to manage access to your organization | Invita a usuarios y agrégalos a roles para administrar el acceso a tu organización | |
|
||||||
|
| Users | Usuarios | sidebar item |
|
||||||
|
| Roles | Roles | sidebar item |
|
||||||
|
| **User tab** | **Pestaña de usuario** | |
|
||||||
|
| Search users | Buscar usuarios | placeholder |
|
||||||
|
| Invite User | Invitar usuario | addbutton |
|
||||||
|
| Email | Email | table header |
|
||||||
|
| Status | Estado | table header |
|
||||||
|
| Role | Role | table header |
|
||||||
|
| Confirmed | Confirmado | account status |
|
||||||
|
| Not confirmed (?) | No confirmado (?) | unknown for me account status |
|
||||||
|
| Owner | Dueño | role |
|
||||||
|
| Admin | Administrador | role |
|
||||||
|
| Member | Miembro | role |
|
||||||
|
| **Roles Tab** | **Pestaña Roles** | |
|
||||||
|
| Search roles | Buscar roles | placeholder |
|
||||||
|
| Add Role | Agregar rol | addbutton |
|
||||||
|
| Name | Nombre | table header |
|
||||||
|
| Description | Descripción | table header |
|
||||||
|
| Admin | Administrador | role |
|
||||||
|
| Member | Miembro | role |
|
||||||
|
| Admin role with the most permissions | Rol de administrador con más permisos | admin role desc |
|
||||||
|
| Members can only view resources | Los miembros sólo pueden ver los recursos | member role desc |
|
||||||
|
|
||||||
|
##### Invite User popup
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ----------------- | ------------------------------------------------------- | ----------- |
|
||||||
|
| Invite User | Invitar usuario | |
|
||||||
|
| Email | Email | |
|
||||||
|
| Enter an email | Introduzca un email | placeholder |
|
||||||
|
| Role | Rol | |
|
||||||
|
| Select role | Seleccionar rol | placeholder |
|
||||||
|
| Gültig für | Válido para | |
|
||||||
|
| 1 day | 1 día | |
|
||||||
|
| 2 days | 2 días | |
|
||||||
|
| 3 days | 3 días | |
|
||||||
|
| 4 days | 4 días | |
|
||||||
|
| 5 days | 5 días | |
|
||||||
|
| 6 days | 6 días | |
|
||||||
|
| 7 days | 7 días | |
|
||||||
|
| Create Invitation | Crear invitación | |
|
||||||
|
| Close | Cerrar | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “Shareable Links”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----- |
|
||||||
|
| Shareable Links | Enlaces para compartir | |
|
||||||
|
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Crear enlaces que se puedan compartir a tus recursos. Los enlaces proporcionan acceso temporal o ilimitado a tu recurso. Puedes configurar la duración de caducidad del enlace cuando lo creas. | |
|
||||||
|
| Easy to create and share | Fácil de crear y compartir | |
|
||||||
|
| Configurable expiration duration | Duración de expiración configurable | |
|
||||||
|
| Secure and revocable | Seguro y revocable | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------- |
|
||||||
|
| Manage Shareable Links | Administrar enlaces compartibles | |
|
||||||
|
| Create shareable links to grant temporary or permanent access to your resources | Crear enlaces compartibles para otorgar acceso temporal o permanente a tus recursos | |
|
||||||
|
| Search links | Buscar enlaces | placeholder |
|
||||||
|
| Create Share Link | Crear enlace para compartir | addbutton |
|
||||||
|
| Resource | Recurso | table header |
|
||||||
|
| Title | Título | table header |
|
||||||
|
| Created | Creado | table header |
|
||||||
|
| Expires | Caduca | table header |
|
||||||
|
| No links. Create one to get started. | No hay enlaces. Crea uno para comenzar. | table placeholder |
|
||||||
|
|
||||||
|
##### Create Shareable Link popup
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ----------------------- |
|
||||||
|
| Create Shareable Link | Crear un enlace para compartir | |
|
||||||
|
| Anyone with this link can access the resource | Cualquier persona con este enlace puede acceder al recurso. | |
|
||||||
|
| Resource | Recurso | |
|
||||||
|
| Select resource | Seleccionar recurso | |
|
||||||
|
| Search resources… | Buscar recursos… | resource selector popup |
|
||||||
|
| Title (optional) | Título (opcional) | |
|
||||||
|
| Enter title | Introducir título | placeholder |
|
||||||
|
| Expire in | Caduca en | |
|
||||||
|
| Minutes | Minutos | |
|
||||||
|
| Hours | Horas | |
|
||||||
|
| Days | Días | |
|
||||||
|
| Months | Meses | |
|
||||||
|
| Years | Años | |
|
||||||
|
| Never expire | Nunca caduca | |
|
||||||
|
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | El tiempo de expiración es el tiempo durante el cual el enlace se podrá utilizar y brindará acceso al recurso. Después de este tiempo, el enlace dejará de funcionar y los usuarios que lo hayan utilizado perderán el acceso al recurso. | |
|
||||||
|
| Create Link | Crear enlace | |
|
||||||
|
| Close | Cerrar | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “General”
|
||||||
|
|
||||||
|
| EN | ES | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------ |
|
||||||
|
| General | General | |
|
||||||
|
| Configure your organization’s general settings | Configura los ajustes generales de tu organización | |
|
||||||
|
| General | General | sidebar item |
|
||||||
|
| Organization Settings | Configuración de la organización | |
|
||||||
|
| Manage your organization details and configuration | Administra los detalles y la configuración de tu organización| |
|
||||||
|
| Name | Nombre | |
|
||||||
|
| This is the display name of the org | Este es el nombre para mostrar de la organización. | |
|
||||||
|
| Save Settings | Guardar configuración | |
|
||||||
|
| Danger Zone | Zona de peligro | |
|
||||||
|
| Once you delete this org, there is no going back. Please be certain. | Una vez que elimines esta organización, no habrá vuelta atrás. Asegúrate de hacerlo. | |
|
||||||
|
| Delete Organization Data | Eliminar datos de la organización | |
|
287
internationalization/pl.md
Normal file
|
@ -0,0 +1,287 @@
|
||||||
|
## Authentication Site
|
||||||
|
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| -------------------------------------------------------- | ------------------------------------------------------------ | ---------- |
|
||||||
|
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Zasilane przez [Pangolin](https://github.com/fosrl/pangolin) | |
|
||||||
|
| Authentication Required | Wymagane uwierzytelnienie | |
|
||||||
|
| Choose your preferred method to access {resource} | Wybierz preferowaną metodę dostępu do {resource} | |
|
||||||
|
| PIN | PIN | |
|
||||||
|
| User | Zaloguj | |
|
||||||
|
| 6-digit PIN Code | 6-cyfrowy kod PIN | pin login |
|
||||||
|
| Login in with PIN | Zaloguj się PIN’em | pin login |
|
||||||
|
| Email | Email | user login |
|
||||||
|
| Enter your email | Wprowadź swój email | user login |
|
||||||
|
| Password | Hasło | user login |
|
||||||
|
| Enter your password | Wprowadź swoje hasło | user login |
|
||||||
|
| Forgot your password? | Zapomniałeś hasła? | user login |
|
||||||
|
| Log in | Zaloguj | user login |
|
||||||
|
|
||||||
|
|
||||||
|
## Login site
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| --------------------- | ------------------------------ | ----------- |
|
||||||
|
| Welcome to Pangolin | Witaj w Pangolin | |
|
||||||
|
| Log in to get started | Zaloguj się, aby rozpocząć<br> | |
|
||||||
|
| Email | Email | |
|
||||||
|
| Enter your email | Wprowadź swój adres e-mail<br> | placeholder |
|
||||||
|
| Password | Hasło | |
|
||||||
|
| Enter your password | Wprowadź swoje hasło | placeholder |
|
||||||
|
| Forgot your password? | Nie pamiętasz hasła? | |
|
||||||
|
| Log in | Zaloguj | |
|
||||||
|
|
||||||
|
# Ogranization site after successful login
|
||||||
|
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ----------------------------------------- | ------------------------------------------ | ----- |
|
||||||
|
| Welcome to Pangolin | Witaj w Pangolin | |
|
||||||
|
| You're a member of {number} organization. | Jesteś użytkownikiem {number} organizacji. | |
|
||||||
|
|
||||||
|
## Shared Header, Navbar and Footer
|
||||||
|
##### Header
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ------------------- | ------------------ | ----- |
|
||||||
|
| Documentation | Dokumentacja | |
|
||||||
|
| Support | Wsparcie | |
|
||||||
|
| Organization {name} | Organizacja {name} | |
|
||||||
|
##### Organization selector
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ---------------- | ---------------- | ----- |
|
||||||
|
| Search… | Szukaj… | |
|
||||||
|
| Create | Utwórz | |
|
||||||
|
| New Organization | Nowa organizacja | |
|
||||||
|
| Organizations | Organizacje | |
|
||||||
|
|
||||||
|
##### Navbar
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| --------------- | ---------------------- | ----- |
|
||||||
|
| Sites | Witryny | |
|
||||||
|
| Resources | Zasoby | |
|
||||||
|
| User & Roles | Użytkownicy i Role | |
|
||||||
|
| Shareable Links | Łącza do udostępniania | |
|
||||||
|
| General | Ogólne | |
|
||||||
|
##### Footer
|
||||||
|
| EN | PL | |
|
||||||
|
| ------------------------- | -------------------------- | -------------- |
|
||||||
|
| Page {number} of {number} | Strona {number} z {number} | |
|
||||||
|
| Rows per page | Wierszy na stronę | |
|
||||||
|
| Pangolin | Pangolin | bottom of site |
|
||||||
|
| Built by Fossorial | Stworzone przez Fossorial | bottom of site |
|
||||||
|
| Open Source | Open source | bottom of site |
|
||||||
|
| Documentation | Dokumentacja | bottom of site |
|
||||||
|
| {version} | {version} | bottom of site |
|
||||||
|
## Main “Sites”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
||||||
|
| Newt (Recommended) | Newt (zalecane) | |
|
||||||
|
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | Aby zapewnić najlepsze doświadczenie użytkownika, korzystaj z Newt. Wykorzystuje on technologię WireGuard w tle i pozwala na dostęp do Twoich prywatnych zasobów za pomocą ich adresu LAN w prywatnej sieci bezpośrednio z poziomu pulpitu nawigacyjnego Pangolin. | |
|
||||||
|
| Runs in Docker | Działa w Dockerze | |
|
||||||
|
| Runs in shell on macOS, Linux, and Windows | Działa w powłoce na systemach macOS, Linux i Windows | |
|
||||||
|
| Install Newt | Zainstaluj Newt | |
|
||||||
|
| Podstawowy WireGuard<br> | Użyj dowolnego klienta WireGuard, aby się połączyć. Będziesz musiał uzyskiwać dostęp do swoich wewnętrznych zasobów za pomocą adresu IP równorzędnego | |
|
||||||
|
| Compatible with all WireGuard clients<br> | Kompatybilny ze wszystkimi klientami WireGuard<br> | |
|
||||||
|
| Manual configuration required | Wymagana ręczna konfiguracja<br> | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| --------------------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------- |
|
||||||
|
| Manage Sites | Zarządzanie witrynami | |
|
||||||
|
| Allow connectivity to your network through secure tunnels | Zezwalaj na łączność z Twoją siecią za pośrednictwem bezpiecznych tuneli | |
|
||||||
|
| Search sites | Szukaj witryny | placeholder |
|
||||||
|
| Add Site | Dodaj witrynę | |
|
||||||
|
| Name | Nazwa | table header |
|
||||||
|
| Online | Status | table header |
|
||||||
|
| Site | Witryna | table header |
|
||||||
|
| Data In | Dane wchodzące | table header |
|
||||||
|
| Data Out | Dane wychodzące | table header |
|
||||||
|
| Connection Type | Typ połączenia | table header |
|
||||||
|
| Online | Online | site state |
|
||||||
|
| Offline | Poza siecią | site state |
|
||||||
|
| Edit → | Edytuj → | |
|
||||||
|
| View settings | Pokaż ustawienia | Popup after clicking “…” on site |
|
||||||
|
| Delete | Usuń | Popup after clicking “…” on site |
|
||||||
|
##### Add Site Popup
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ------------------------------------------------------ | --------------------------------------------------- | ----------- |
|
||||||
|
| Create Site | Utwórz witrynę | |
|
||||||
|
| Create a new site to start connection for this site | Utwórz nową witrynę aby rozpocząć połączenie | |
|
||||||
|
| Name | Nazwa | |
|
||||||
|
| Site name | Nazwa witryny | placeholder |
|
||||||
|
| This is the name that will be displayed for this site. | Tak będzie wyświetlana twoja witryna | desc |
|
||||||
|
| Method | Metoda | |
|
||||||
|
| Local | Lokalna | |
|
||||||
|
| Newt | Newt | |
|
||||||
|
| WireGuard | WireGuard | |
|
||||||
|
| This is how you will expose connections. | Tak będą eksponowane połączenie. | |
|
||||||
|
| You will only be able to see the configuration once. | Tą konfigurację możesz zobaczyć tylko raz. | |
|
||||||
|
| Learn how to install Newt on your system | Dowiedz się jak zainstalować Newt na twoim systemie | |
|
||||||
|
| I have copied the config | Skopiowałem konfigurację | |
|
||||||
|
| Create Site | Utwórz witrynę | |
|
||||||
|
| Close | Zamknij | |
|
||||||
|
|
||||||
|
## Main “Resources”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
||||||
|
| Resources | Zasoby | |
|
||||||
|
| Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | Zasoby to serwery proxy dla aplikacji działających w Twojej prywatnej sieci. Utwórz zasób dla dowolnej aplikacji HTTP lub HTTPS w swojej prywatnej sieci. Każdy zasób musi być połączony z witryną, aby umożliwić prywatne i bezpieczne połączenie przez szyfrowany tunel WireGuard. | |
|
||||||
|
| Secure connectivity with WireGuard encryption | Bezpieczna łączność z szyfrowaniem WireGuard | |
|
||||||
|
| Configure multiple authentication methods | Konfigurowanie wielu metod uwierzytelniania | |
|
||||||
|
| User and role-based access control | Kontrola dostępu oparta na użytkownikach i rolach | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| -------------------------------------------------- | -------------------------------------------------------------- | -------------------- |
|
||||||
|
| Manage Resources | Zarządzaj zasobami | |
|
||||||
|
| Create secure proxies to your private applications | Twórz bezpieczne serwery proxy dla swoich prywatnych aplikacji | |
|
||||||
|
| Search resources | Szukaj w zasobach | placeholder |
|
||||||
|
| Name | Nazwa | |
|
||||||
|
| Site | Witryna | |
|
||||||
|
| Full URL | Pełny URL | |
|
||||||
|
| Authentication | Uwierzytelnianie | |
|
||||||
|
| Not Protected | Niezabezpieczony | authentication state |
|
||||||
|
| Protected | Zabezpieczony | authentication state |
|
||||||
|
| Edit → | Edytuj → | |
|
||||||
|
| Add Resource | Dodaj zasób | |
|
||||||
|
##### Add Resource Popup
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------------- |
|
||||||
|
| Create Resource | Utwórz zasób | |
|
||||||
|
| Create a new resource to proxy request to your app | Utwórz nowy zasób, aby przekazywać żądania do swojej aplikacji | |
|
||||||
|
| Name | Nazwa | |
|
||||||
|
| My Resource | Nowy zasób | name placeholder |
|
||||||
|
| This is the name that will be displayed for this resource. | To jest nazwa, która będzie wyświetlana dla tego zasobu | |
|
||||||
|
| Subdomain | Subdomena | |
|
||||||
|
| Enter subdomain | Wprowadź subdomenę | |
|
||||||
|
| This is the fully qualified domain name that will be used to access the resource. | To jest pełna nazwa domeny, która będzie używana do dostępu do zasobu. | |
|
||||||
|
| Site | Witryna | |
|
||||||
|
| Search site… | Szukaj witryny… | Site selector popup |
|
||||||
|
| This is the site that will be used in the dashboard. | To jest witryna, która będzie używana w pulpicie nawigacyjnym. | |
|
||||||
|
| Create Resource | Utwórz zasób | |
|
||||||
|
| Close | Zamknij | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “User & Roles”
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------- |
|
||||||
|
| Manage User & Roles | Zarządzanie użytkownikami i rolami | |
|
||||||
|
| Invite users and add them to roles to manage access to your organization | Zaproś użytkowników i przypisz im role, aby zarządzać dostępem do Twojej organizacji | |
|
||||||
|
| Users | Użytkownicy | sidebar item |
|
||||||
|
| Roles | Role | sidebar item |
|
||||||
|
| **User tab** | | |
|
||||||
|
| Search users | Wyszukaj użytkownika | placeholder |
|
||||||
|
| Invite User | Zaproś użytkownika | addbutton |
|
||||||
|
| Email | Email | table header |
|
||||||
|
| Status | Status | table header |
|
||||||
|
| Role | Rola | table header |
|
||||||
|
| Confirmed | Zatwierdzony | account status |
|
||||||
|
| Not confirmed (?) | Niezatwierdzony (?) | unknown for me account status |
|
||||||
|
| Owner | Właściciel | role |
|
||||||
|
| Admin | Administrator | role |
|
||||||
|
| Member | Użytkownik | role |
|
||||||
|
| **Roles Tab** | | |
|
||||||
|
| Search roles | Wyszukaj role | placeholder |
|
||||||
|
| Add Role | Dodaj role | addbutton |
|
||||||
|
| Name | Nazwa | table header |
|
||||||
|
| Description | Opis | table header |
|
||||||
|
| Admin | Administrator | role |
|
||||||
|
| Member | Użytkownik | role |
|
||||||
|
| Admin role with the most permissions | Rola administratora z najszerszymi uprawnieniami | admin role desc |
|
||||||
|
| Members can only view resources | Członkowie mogą jedynie przeglądać zasoby | member role desc |
|
||||||
|
|
||||||
|
##### Invite User popup
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ----------------- | ------------------------------------------ | ----------- |
|
||||||
|
| Invite User | Give new users access to your organization | |
|
||||||
|
| Email | Email | |
|
||||||
|
| Enter an email | Wprowadź email | placeholder |
|
||||||
|
| Role | Rola | |
|
||||||
|
| Select role | Wybierz role | placeholder |
|
||||||
|
| Vaild for | Ważne do | |
|
||||||
|
| 1 day | Dzień | |
|
||||||
|
| 2 days | 2 dni | |
|
||||||
|
| 3 days | 3 dni | |
|
||||||
|
| 4 days | 4 dni | |
|
||||||
|
| 5 days | 5 dni | |
|
||||||
|
| 6 days | 6 dni | |
|
||||||
|
| 7 days | 7 dni | |
|
||||||
|
| Create Invitation | Utwórz zaproszenie | |
|
||||||
|
| Close | Zamknij | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “Shareable Links”
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||||
|
| Shareable Links | Łącza do udostępniania | |
|
||||||
|
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Twórz linki do udostępniania swoich zasobów. Linki zapewniają tymczasowy lub nieograniczony dostęp do zasobu. Możesz skonfigurować czas wygaśnięcia linku podczas jego tworzenia. | |
|
||||||
|
| Easy to create and share | Łatwe tworzenie i udostępnianie | |
|
||||||
|
| Configurable expiration duration | Konfigurowalny czas wygaśnięcia | |
|
||||||
|
| Secure and revocable | Bezpieczne i odwołalne | |
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ----------------- |
|
||||||
|
| Manage Shareable Links | Zarządzaj łączami do udostępniania | |
|
||||||
|
| Create shareable links to grant temporary or permament access to your resources | Utwórz łącze do udostępniania w celu przyznania tymczasowego lub stałego dostępu do zasobów | |
|
||||||
|
| Search links | Szukaj łączy | placeholder |
|
||||||
|
| Create Share Link | Utwórz nowe łącze | addbutton |
|
||||||
|
| Resource | Zasób | table header |
|
||||||
|
| Title | Tytuł | table header |
|
||||||
|
| Created | Utworzone | table header |
|
||||||
|
| Expires | Wygasa | table header |
|
||||||
|
| No links. Create one to get started. | Brak łączy. Utwórz, aby rozpocząć. | table placeholder |
|
||||||
|
|
||||||
|
##### Create Shareable Link popup
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||||
|
| Create Shareable Link | Utwórz łącze do udostępnienia | |
|
||||||
|
| Anyone with this link can access the resource | Każdy kto ma ten link może korzystać z zasobu | |
|
||||||
|
| Resource | Zasób | |
|
||||||
|
| Select resource | Wybierz zasób | |
|
||||||
|
| Search resources… | Szukaj zasobów… | resource selector popup |
|
||||||
|
| Title (optional) | Tytuł (opcjonalny) | |
|
||||||
|
| Enter title | Wprowadź tytuł | placeholder |
|
||||||
|
| Expire in | Wygasa za | |
|
||||||
|
| Minutes | Minut | |
|
||||||
|
| Hours | Godzin | |
|
||||||
|
| Days | Dni | |
|
||||||
|
| Months | Miesięcy | |
|
||||||
|
| Years | Lat | |
|
||||||
|
| Never expire | Nie wygasa | |
|
||||||
|
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Czas wygaśnięcia to okres, przez który link będzie aktywny i zapewni dostęp do zasobu. Po upływie tego czasu link przestanie działać, a użytkownicy, którzy go użyli, stracą dostęp do zasobu. | |
|
||||||
|
| Create Link | Utwórz łącze | |
|
||||||
|
| Close | Zamknij | |
|
||||||
|
|
||||||
|
|
||||||
|
## Main “General”
|
||||||
|
|
||||||
|
| EN | PL | Notes |
|
||||||
|
| -------------------------------------------------------------------- | ------------------------------------------------------------------- | ------------ |
|
||||||
|
| General | Ogólne | |
|
||||||
|
| Configure your organization’s general settings | Zarządzaj ogólnymi ustawieniami twoich organizacji | |
|
||||||
|
| General | Ogólne | sidebar item |
|
||||||
|
| Organization Settings | Ustawienia organizacji | |
|
||||||
|
| Manage your organization details and configuration | Zarządzaj szczegółami i konfiguracją organizacji | |
|
||||||
|
| Name | Nazwa | |
|
||||||
|
| This is the display name of the org | To jest wyświetlana nazwa Twojej organizacji | |
|
||||||
|
| Save Settings | Zapisz ustawienia | |
|
||||||
|
| Danger Zone | Niebezpieczna strefa | |
|
||||||
|
| Once you delete this org, there is no going back. Please be certain. | Jeśli usuniesz swoją tą organizację, nie ma odwrotu. Bądź ostrożny! | |
|
||||||
|
| Delete Organization Data | Usuń dane organizacji | |
|
310
internationalization/tr.md
Normal file
|
@ -0,0 +1,310 @@
|
||||||
|
## Authentication Site
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| -------------------------------------------------------- | ---------------------------------------------------------------------------------- | ---------- |
|
||||||
|
| Powered by [Pangolin](https://github.com/fosrl/pangolin) | Pangolin Tarafından Destekleniyor | |
|
||||||
|
| Authentication Required | Kimlik Doğrulaması Gerekli | |
|
||||||
|
| Choose your preferred method to access {resource} | {resource}'a erişmek için tercih ettiğiniz yöntemi seçin | |
|
||||||
|
| PIN | PIN | |
|
||||||
|
| User | Kullanıcı | |
|
||||||
|
| 6-digit PIN Code | 6 haneli PIN Kodu | pin login |
|
||||||
|
| Login in with PIN | PIN ile Giriş Yap | pin login |
|
||||||
|
| Email | E-posta | user login |
|
||||||
|
| Enter your email | E-postanızı girin | user login |
|
||||||
|
| Password | Şifre | user login |
|
||||||
|
| Enter your password | Şifrenizi girin | user login |
|
||||||
|
| Forgot your password? | Şifrenizi mi unuttunuz? | user login |
|
||||||
|
| Log in | Giriş Yap | user login |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Login site
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| --------------------- | ------------------------------------------------------ | ----------- |
|
||||||
|
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
|
||||||
|
| Log in to get started | Başlamak için giriş yapın | |
|
||||||
|
| Email | E-posta | |
|
||||||
|
| Enter your email | E-posta adresinizi girin | placeholder |
|
||||||
|
| Password | Şifre | |
|
||||||
|
| Enter your password | Şifrenizi girin | placeholder |
|
||||||
|
| Forgot your password? | Şifrenizi mi unuttunuz? | |
|
||||||
|
| Log in | Giriş Yap | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Organization site after successful login
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ----------------------------------------- | ------------------------------------------------------------------- | ----- |
|
||||||
|
| Welcome to Pangolin | Pangolin'e Hoşgeldiniz | |
|
||||||
|
| You're a member of {number} organization. | {number} organizasyonunun üyesiniz. | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shared Header, Navbar and Footer
|
||||||
|
|
||||||
|
##### Header
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------- | -------------------------- | ----- |
|
||||||
|
| Documentation | Dokümantasyon | |
|
||||||
|
| Support | Destek | |
|
||||||
|
| Organization {name} | Organizasyon {name} | |
|
||||||
|
|
||||||
|
##### Organization selector
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ---------------- | ---------------------- | ----- |
|
||||||
|
| Search… | Ara… | |
|
||||||
|
| Create | Oluştur | |
|
||||||
|
| New Organization | Yeni Organizasyon | |
|
||||||
|
| Organizations | Organizasyonlar | |
|
||||||
|
|
||||||
|
##### Navbar
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| --------------- | ------------------------------- | ----- |
|
||||||
|
| Sites | Siteler | |
|
||||||
|
| Resources | Kaynaklar | |
|
||||||
|
| User & Roles | Kullanıcılar ve Roller | |
|
||||||
|
| Shareable Links | Paylaşılabilir Linkler | |
|
||||||
|
| General | Genel | |
|
||||||
|
|
||||||
|
##### Footer
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------- | ------------------------------------------------ | -------------------- |
|
||||||
|
| Page {number} of {number} | Sayfa {number} / {number} | |
|
||||||
|
| Rows per page | Sayfa başına satırlar | |
|
||||||
|
| Pangolin | Pangolin | Footer'da yer alır |
|
||||||
|
| Built by Fossorial | Fossorial tarafından oluşturuldu | Footer'da yer alır |
|
||||||
|
| Open Source | Açık Kaynak | Footer'da yer alır |
|
||||||
|
| Documentation | Dokümantasyon | Footer'da yer alır |
|
||||||
|
| {version} | {version} | Footer'da yer alır |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main “Sites”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||||
|
| Newt (Recommended) | Newt (Tavsiye Edilen) | |
|
||||||
|
| For the best user experience, use Newt. It uses WireGuard under the hood and allows you to address your private resources by their LAN address on your private network from within the Pangolin dashboard. | En iyi kullanıcı deneyimi için Newt'i kullanın. Newt, arka planda WireGuard kullanır ve Pangolin kontrol paneli üzerinden özel ağınızdaki kaynaklarınıza LAN adresleriyle erişmenizi sağlar. | |
|
||||||
|
| Runs in Docker | Docker üzerinde çalışır | |
|
||||||
|
| Runs in shell on macOS, Linux, and Windows | macOS, Linux ve Windows’ta komut satırında çalışır | |
|
||||||
|
| Install Newt | Newt'i Yükle | |
|
||||||
|
| Basic WireGuard<br> | Temel WireGuard<br> | |
|
||||||
|
| Compatible with all WireGuard clients<br> | Tüm WireGuard istemcileriyle uyumlu<br> | |
|
||||||
|
| Manual configuration required | Manuel yapılandırma gereklidir | |
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| --------------------------------------------------------- | --------------------------------------------------------------------------- | ------------ |
|
||||||
|
| Manage Sites | Siteleri Yönet | |
|
||||||
|
| Allow connectivity to your network through secure tunnels | Güvenli tüneller aracılığıyla ağınıza bağlantı sağlayın | |
|
||||||
|
| Search sites | Siteleri ara | placeholder |
|
||||||
|
| Add Site | Site Ekle | |
|
||||||
|
| Name | Ad | Table Header |
|
||||||
|
| Online | Çevrimiçi | Table Header |
|
||||||
|
| Site | Site | Table Header |
|
||||||
|
| Data In | Gelen Veri | Table Header |
|
||||||
|
| Data Out | Giden Veri | Table Header |
|
||||||
|
| Connection Type | Bağlantı Türü | Table Header |
|
||||||
|
| Online | Çevrimiçi | Site state |
|
||||||
|
| Offline | Çevrimdışı | Site state |
|
||||||
|
| Edit → | Düzenle → | |
|
||||||
|
| View settings | Ayarları Görüntüle | Popup |
|
||||||
|
| Delete | Sil | Popup |
|
||||||
|
|
||||||
|
##### Add Site Popup
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ----------- |
|
||||||
|
| Create Site | Site Oluştur | |
|
||||||
|
| Create a new site to start connection for this site | Bu site için bağlantıyı başlatmak amacıyla yeni bir site oluşturun | |
|
||||||
|
| Name | Ad | |
|
||||||
|
| Site name | Site adı | placeholder |
|
||||||
|
| This is the name that will be displayed for this site. | Bu, site için görüntülenecek addır. | desc |
|
||||||
|
| Method | Yöntem | |
|
||||||
|
| Local | Yerel | |
|
||||||
|
| Newt | Newt | |
|
||||||
|
| WireGuard | WireGuard | |
|
||||||
|
| This is how you will expose connections. | Bağlantılarınızı bu şekilde açığa çıkaracaksınız. | |
|
||||||
|
| You will only be able to see the configuration once. | Yapılandırmayı yalnızca bir kez görüntüleyebilirsiniz. | |
|
||||||
|
| Learn how to install Newt on your system | Sisteminizde Newt'in nasıl kurulacağını öğrenin | |
|
||||||
|
| I have copied the config | Yapılandırmayı kopyaladım | |
|
||||||
|
| Create Site | Site Oluştur | |
|
||||||
|
| Close | Kapat | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main “Resources”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ | ----- |
|
||||||
|
| Resources | Kaynaklar | |
|
||||||
|
| Ressourcen sind Proxy-Server für Anwendungen, die in Ihrem privaten Netzwerk laufen. Erstellen Sie eine Ressource für jede HTTP- oder HTTPS-Anwendung in Ihrem privaten Netzwerk. Jede Ressource muss mit einer Website verbunden sein, um eine private und sichere Verbindung über den verschlüsselten WireGuard-Tunnel zu ermöglichen. | Kaynaklar, özel ağınızda çalışan uygulamalar için proxy sunucularıdır. Özel ağınızdaki her HTTP veya HTTPS uygulaması için bir kaynak oluşturun. Her kaynağın, şifrelenmiş WireGuard tüneli üzerinden özel ve güvenli bağlantı sağlamak üzere bir siteyle ilişkili olması gerekir. | |
|
||||||
|
| Secure connectivity with WireGuard encryption | WireGuard şifrelemesiyle güvenli bağlantı | |
|
||||||
|
| Configure multiple authentication methods | Birden çok kimlik doğrulama yöntemini yapılandırın | |
|
||||||
|
| User and role-based access control | Kullanıcı ve role dayalı erişim kontrolü | |
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| -------------------------------------------------- | ------------------------------------------------------------- | -------------------- |
|
||||||
|
| Manage Resources | Kaynakları Yönet | |
|
||||||
|
| Create secure proxies to your private applications | Özel uygulamalarınız için güvenli proxy’ler oluşturun | |
|
||||||
|
| Search resources | Kaynakları ara | placeholder |
|
||||||
|
| Name | Ad | |
|
||||||
|
| Site | Site | |
|
||||||
|
| Full URL | Tam URL | |
|
||||||
|
| Authentication | Kimlik Doğrulama | |
|
||||||
|
| Not Protected | Korunmayan | authentication state |
|
||||||
|
| Protected | Korunan | authentication state |
|
||||||
|
| Edit → | Düzenle → | |
|
||||||
|
| Add Resource | Kaynak Ekle | |
|
||||||
|
|
||||||
|
##### Add Resource Popup
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | ------------- |
|
||||||
|
| Create Resource | Kaynak Oluştur | |
|
||||||
|
| Create a new resource to proxy request to your app | Uygulamanıza gelen istekleri yönlendirmek için yeni bir kaynak oluşturun | |
|
||||||
|
| Name | Ad | |
|
||||||
|
| My Resource | Kaynağım | name placeholder |
|
||||||
|
| This is the name that will be displayed for this resource. | Bu, kaynağın görüntülenecek adıdır. | |
|
||||||
|
| Subdomain | Alt alan adı | |
|
||||||
|
| Enter subdomain | Alt alan adını girin | |
|
||||||
|
| This is the fully qualified domain name that will be used to access the resource. | Kaynağa erişmek için kullanılacak tam nitelikli alan adıdır. | |
|
||||||
|
| Site | Site | |
|
||||||
|
| Search site… | Site ara… | Site selector popup |
|
||||||
|
| This is the site that will be used in the dashboard. | Kontrol panelinde kullanılacak sitedir. | |
|
||||||
|
| Create Resource | Kaynak Oluştur | |
|
||||||
|
| Close | Kapat | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main “User & Roles”
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | ----------------------------- |
|
||||||
|
| Manage User & Roles | Kullanıcılar ve Rolleri Yönet | |
|
||||||
|
| Invite users and add them to roles to manage access to your organization | Organizasyonunuza erişimi yönetmek için kullanıcıları davet edin ve rollere atayın | |
|
||||||
|
| Users | Kullanıcılar | sidebar item |
|
||||||
|
| Roles | Roller | sidebar item |
|
||||||
|
| **User tab** | **Kullanıcı Sekmesi** | |
|
||||||
|
| Search users | Kullanıcıları ara | placeholder |
|
||||||
|
| Invite User | Kullanıcı Davet Et | addbutton |
|
||||||
|
| Email | E-posta | table header |
|
||||||
|
| Status | Durum | table header |
|
||||||
|
| Role | Rol | table header |
|
||||||
|
| Confirmed | Onaylandı | account status |
|
||||||
|
| Not confirmed (?) | Onaylanmadı (?) | account status |
|
||||||
|
| Owner | Sahip | role |
|
||||||
|
| Admin | Yönetici | role |
|
||||||
|
| Member | Üye | role |
|
||||||
|
| **Roles Tab** | **Roller Sekmesi** | |
|
||||||
|
| Search roles | Rolleri ara | placeholder |
|
||||||
|
| Add Role | Rol Ekle | addbutton |
|
||||||
|
| Name | Ad | table header |
|
||||||
|
| Description | Açıklama | table header |
|
||||||
|
| Admin | Yönetici | role |
|
||||||
|
| Member | Üye | role |
|
||||||
|
| Admin role with the most permissions | En fazla yetkiye sahip yönetici rolü | admin role desc |
|
||||||
|
| Members can only view resources | Üyeler yalnızca kaynakları görüntüleyebilir | member role desc |
|
||||||
|
|
||||||
|
##### Invite User popup
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ----------------- | ----------------------------------------------------------------------- | ----------- |
|
||||||
|
| Invite User | Kullanıcı Davet Et | |
|
||||||
|
| Email | E-posta | |
|
||||||
|
| Enter an email | Bir e-posta adresi girin | placeholder |
|
||||||
|
| Role | Rol | |
|
||||||
|
| Select role | Rol seçin | placeholder |
|
||||||
|
| Gültig für | Geçerlilik Süresi | |
|
||||||
|
| 1 day | 1 gün | |
|
||||||
|
| 2 days | 2 gün | |
|
||||||
|
| 3 days | 3 gün | |
|
||||||
|
| 4 days | 4 gün | |
|
||||||
|
| 5 days | 5 gün | |
|
||||||
|
| 6 days | 6 gün | |
|
||||||
|
| 7 days | 7 gün | |
|
||||||
|
| Create Invitation | Davetiye Oluştur | |
|
||||||
|
| Close | Kapat | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main “Shareable Links”
|
||||||
|
|
||||||
|
##### “Hero” section
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------- | ----- |
|
||||||
|
| Shareable Links | Paylaşılabilir Bağlantılar | |
|
||||||
|
| Create shareable links to your resources. Links provide temporary or unlimited access to your resource. You can configure the expiration duration of the link when you create one. | Kaynaklarınıza paylaşılabilir bağlantılar oluşturun. Bağlantılar, kaynağınıza geçici veya sınırsız erişim sağlar. Oluştururken bağlantının geçerlilik süresini ayarlayabilirsiniz. | |
|
||||||
|
| Easy to create and share | Oluşturması ve paylaşması kolay | |
|
||||||
|
| Configurable expiration duration | Yapılandırılabilir geçerlilik süresi | |
|
||||||
|
| Secure and revocable | Güvenli ve iptal edilebilir | |
|
||||||
|
|
||||||
|
##### Content
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------- |
|
||||||
|
| Manage Shareable Links | Paylaşılabilir Bağlantıları Yönet | |
|
||||||
|
| Create shareable links to grant temporary or permanent access to your resources | Kaynaklarınıza geçici veya kalıcı erişim sağlamak için paylaşılabilir bağlantılar oluşturun | |
|
||||||
|
| Search links | Bağlantıları ara | placeholder |
|
||||||
|
| Create Share Link | Bağlantı Oluştur | addbutton |
|
||||||
|
| Resource | Kaynak | table header |
|
||||||
|
| Title | Başlık | table header |
|
||||||
|
| Created | Oluşturulma Tarihi | table header |
|
||||||
|
| Expires | Son Kullanma Tarihi | table header |
|
||||||
|
| No links. Create one to get started. | Bağlantı yok. Başlamak için bir tane oluşturun. | table placeholder |
|
||||||
|
|
||||||
|
##### Create Shareable Link popup
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- | ----------------------- |
|
||||||
|
| Create Shareable Link | Paylaşılabilir Bağlantı Oluştur | |
|
||||||
|
| Anyone with this link can access the resource | Bu bağlantıya sahip olan herkes kaynağa erişebilir | |
|
||||||
|
| Resource | Kaynak | |
|
||||||
|
| Select resource | Kaynak seçin | |
|
||||||
|
| Search resources… | Kaynak ara… | resource selector popup |
|
||||||
|
| Title (optional) | Başlık (isteğe bağlı) | |
|
||||||
|
| Enter title | Başlık girin | placeholder |
|
||||||
|
| Expire in | Sona Erme Süresi | |
|
||||||
|
| Minutes | Dakika | |
|
||||||
|
| Hours | Saat | |
|
||||||
|
| Days | Gün | |
|
||||||
|
| Months | Ay | |
|
||||||
|
| Years | Yıl | |
|
||||||
|
| Never expire | Asla sona erme | |
|
||||||
|
| Expiration time is how long the link will be usable and provide access to the resource. After this time, the link will no longer work, and users who used this link will lose access to the resource. | Bağlantının geçerlilik süresi, bağlantının ne kadar süreyle kullanılabilir olacağını ve kaynağa erişim sağlayacağını belirler. Bu sürenin sonunda bağlantı çalışmaz hale gelir ve bağlantıyı kullananlar kaynağa erişimini kaybeder. | |
|
||||||
|
| Create Link | Bağlantı Oluştur | |
|
||||||
|
| Close | Kapat | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Main “General”
|
||||||
|
|
||||||
|
| EN | TR | Notes |
|
||||||
|
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | ------------ |
|
||||||
|
| General | Genel | |
|
||||||
|
| Configure your organization’s general settings | Organizasyonunuzun genel ayarlarını yapılandırın | |
|
||||||
|
| General | Genel | sidebar item |
|
||||||
|
| Organization Settings | Organizasyon Ayarları | |
|
||||||
|
| Manage your organization details and configuration | Organizasyonunuzun detaylarını ve yapılandırmasını yönetin | |
|
||||||
|
| Name | Ad | |
|
||||||
|
| This is the display name of the org | Bu, organizasyonunuzun görüntülenecek adıdır. | |
|
||||||
|
| Save Settings | Ayarları Kaydet | |
|
||||||
|
| Danger Zone | Tehlikeli Bölge | |
|
||||||
|
| Once you delete this org, there is no going back. Please be certain. | Bu organizasyonu sildikten sonra geri dönüş yoktur. Lütfen emin olun. | |
|
||||||
|
| Delete Organization Data | Organizasyon Verilerini Sil | |
|
1463
messages/bg-BG.json
1463
messages/cs-CZ.json
1463
messages/de-DE.json
1500
messages/en-US.json
1463
messages/es-ES.json
1463
messages/fr-FR.json
1463
messages/it-IT.json
1463
messages/ko-KR.json
1461
messages/nb-NO.json
1463
messages/nl-NL.json
1463
messages/pl-PL.json
1463
messages/pt-PT.json
1463
messages/ru-RU.json
1463
messages/tr-TR.json
1463
messages/zh-CN.json
|
@ -1,8 +1,4 @@
|
||||||
import createNextIntlPlugin from "next-intl/plugin";
|
/** @type {import('next').NextConfig} */
|
||||||
|
|
||||||
const withNextIntl = createNextIntlPlugin();
|
|
||||||
|
|
||||||
/** @type {import("next").NextConfig} */
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true
|
ignoreDuringBuilds: true
|
||||||
|
@ -10,4 +6,4 @@ const nextConfig = {
|
||||||
output: "standalone"
|
output: "standalone"
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withNextIntl(nextConfig);
|
export default nextConfig;
|
||||||
|
|
3361
package-lock.json
generated
115
package.json
|
@ -21,46 +21,43 @@
|
||||||
"db:clear-migrations": "rm -rf server/migrations",
|
"db:clear-migrations": "rm -rf server/migrations",
|
||||||
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
"build:sqlite": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsSqlite.ts -o dist/migrations.mjs",
|
||||||
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
"build:pg": "mkdir -p dist && next build && node esbuild.mjs -e server/index.ts -o dist/server.mjs && node esbuild.mjs -e server/setup/migrationsPg.ts -o dist/migrations.mjs",
|
||||||
"start": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
"start:sqlite": "DB_TYPE=sqlite NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||||
"email": "email dev --dir server/emails/templates --port 3005",
|
"start:pg": "DB_TYPE=pg NODE_OPTIONS=--enable-source-maps NODE_ENV=development ENVIRONMENT=prod sh -c 'node dist/migrations.mjs && node dist/server.mjs'",
|
||||||
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs"
|
"email": "email dev --dir server/emails/templates --port 3005"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@asteasolutions/zod-to-openapi": "^7.3.4",
|
"@asteasolutions/zod-to-openapi": "^7.3.2",
|
||||||
"@hookform/resolvers": "3.9.1",
|
"@hookform/resolvers": "3.9.1",
|
||||||
"@node-rs/argon2": "^2.0.2",
|
"@node-rs/argon2": "^2.0.2",
|
||||||
"@oslojs/crypto": "1.0.1",
|
"@oslojs/crypto": "1.0.1",
|
||||||
"@oslojs/encoding": "1.1.0",
|
"@oslojs/encoding": "1.1.0",
|
||||||
"@radix-ui/react-avatar": "1.1.10",
|
"@radix-ui/react-avatar": "1.1.10",
|
||||||
"@radix-ui/react-checkbox": "1.3.3",
|
"@radix-ui/react-checkbox": "1.3.2",
|
||||||
"@radix-ui/react-collapsible": "1.1.12",
|
"@radix-ui/react-collapsible": "1.1.11",
|
||||||
"@radix-ui/react-dialog": "1.1.15",
|
"@radix-ui/react-dialog": "1.1.14",
|
||||||
"@radix-ui/react-dropdown-menu": "2.1.16",
|
"@radix-ui/react-dropdown-menu": "2.1.15",
|
||||||
"@radix-ui/react-icons": "1.3.2",
|
"@radix-ui/react-icons": "1.3.2",
|
||||||
"@radix-ui/react-label": "2.1.7",
|
"@radix-ui/react-label": "2.1.7",
|
||||||
"@radix-ui/react-popover": "1.1.15",
|
"@radix-ui/react-popover": "1.1.14",
|
||||||
"@radix-ui/react-progress": "^1.1.7",
|
"@radix-ui/react-progress": "^1.1.7",
|
||||||
"@radix-ui/react-radio-group": "1.3.8",
|
"@radix-ui/react-radio-group": "1.3.7",
|
||||||
"@radix-ui/react-scroll-area": "^1.2.10",
|
"@radix-ui/react-scroll-area": "^1.2.9",
|
||||||
"@radix-ui/react-select": "2.2.6",
|
"@radix-ui/react-select": "2.2.5",
|
||||||
"@radix-ui/react-separator": "1.1.7",
|
"@radix-ui/react-separator": "1.1.7",
|
||||||
"@radix-ui/react-slot": "1.2.3",
|
"@radix-ui/react-slot": "1.2.3",
|
||||||
"@radix-ui/react-switch": "1.2.6",
|
"@radix-ui/react-switch": "1.2.5",
|
||||||
"@radix-ui/react-tabs": "1.1.13",
|
"@radix-ui/react-tabs": "1.1.12",
|
||||||
"@radix-ui/react-toast": "1.2.15",
|
"@radix-ui/react-toast": "1.2.14",
|
||||||
"@radix-ui/react-tooltip": "^1.2.8",
|
"@react-email/components": "0.0.41",
|
||||||
"@react-email/components": "0.5.0",
|
"@react-email/render": "^1.1.2",
|
||||||
"@react-email/render": "^1.2.0",
|
"@react-email/tailwind": "1.0.5",
|
||||||
"@react-email/tailwind": "1.2.2",
|
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
|
||||||
"@simplewebauthn/server": "^9.0.3",
|
|
||||||
"@tailwindcss/forms": "^0.5.10",
|
"@tailwindcss/forms": "^0.5.10",
|
||||||
"@tanstack/react-table": "8.21.3",
|
"@tanstack/react-table": "8.21.3",
|
||||||
"arctic": "^3.7.0",
|
"arctic": "^3.7.0",
|
||||||
"axios": "1.11.0",
|
"axios": "1.9.0",
|
||||||
"better-sqlite3": "11.7.0",
|
"better-sqlite3": "11.7.0",
|
||||||
"canvas-confetti": "1.9.3",
|
"canvas-confetti": "1.9.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
"clsx": "2.1.1",
|
"clsx": "2.1.1",
|
||||||
"cmdk": "1.1.1",
|
"cmdk": "1.1.1",
|
||||||
"cookie": "^1.0.2",
|
"cookie": "^1.0.2",
|
||||||
|
@ -68,12 +65,12 @@
|
||||||
"cookies": "^0.9.1",
|
"cookies": "^0.9.1",
|
||||||
"cors": "2.8.5",
|
"cors": "2.8.5",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"drizzle-orm": "0.44.4",
|
"drizzle-orm": "0.38.3",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.28.0",
|
||||||
"eslint-config-next": "15.4.6",
|
"eslint-config-next": "15.3.3",
|
||||||
"express": "5.1.0",
|
"express": "4.21.2",
|
||||||
"express-rate-limit": "8.0.1",
|
"express-rate-limit": "7.5.0",
|
||||||
"glob": "11.0.3",
|
"glob": "11.0.2",
|
||||||
"helmet": "8.1.0",
|
"helmet": "8.1.0",
|
||||||
"http-errors": "2.0.0",
|
"http-errors": "2.0.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
|
@ -81,70 +78,66 @@
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"js-yaml": "4.1.0",
|
"js-yaml": "4.1.0",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lucide-react": "0.539.0",
|
"lucide-react": "0.511.0",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "15.4.6",
|
"next": "15.3.3",
|
||||||
"next-intl": "^4.3.4",
|
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"node-cache": "5.1.2",
|
"node-cache": "5.1.2",
|
||||||
"node-fetch": "3.3.2",
|
"node-fetch": "3.3.2",
|
||||||
"nodemailer": "7.0.5",
|
"nodemailer": "6.9.16",
|
||||||
"npm": "^11.5.2",
|
"npm": "^11.4.1",
|
||||||
"oslo": "1.2.1",
|
"oslo": "1.2.1",
|
||||||
"pg": "^8.16.2",
|
"pg": "^8.16.0",
|
||||||
"posthog-node": "^5.7.0",
|
|
||||||
"qrcode.react": "4.2.0",
|
"qrcode.react": "4.2.0",
|
||||||
"react": "19.1.1",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.1",
|
"react-dom": "19.1.0",
|
||||||
"react-easy-sort": "^1.6.0",
|
"react-easy-sort": "^1.6.0",
|
||||||
"react-hook-form": "7.62.0",
|
"react-hook-form": "7.56.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"rebuild": "0.1.2",
|
"rebuild": "0.1.2",
|
||||||
"semver": "^7.7.2",
|
"semver": "7.7.2",
|
||||||
"swagger-ui-express": "^5.0.1",
|
"swagger-ui-express": "^5.0.1",
|
||||||
"tailwind-merge": "3.3.1",
|
"tailwind-merge": "2.6.0",
|
||||||
"tw-animate-css": "^1.3.7",
|
"tw-animate-css": "^1.3.3",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vaul": "1.1.2",
|
"vaul": "1.1.2",
|
||||||
"winston": "3.17.0",
|
"winston": "3.17.0",
|
||||||
"winston-daily-rotate-file": "5.0.0",
|
"winston-daily-rotate-file": "5.0.0",
|
||||||
"ws": "8.18.3",
|
"ws": "8.18.2",
|
||||||
"yargs": "18.0.0",
|
"zod": "3.25.56",
|
||||||
"zod": "3.25.76",
|
"zod-validation-error": "3.4.1"
|
||||||
"zod-validation-error": "3.5.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "1.49.0",
|
"@dotenvx/dotenvx": "1.44.2",
|
||||||
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
"@esbuild-plugins/tsconfig-paths": "0.1.2",
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.8",
|
||||||
"@types/better-sqlite3": "7.6.12",
|
"@types/better-sqlite3": "7.6.12",
|
||||||
"@types/cookie-parser": "1.4.9",
|
"@types/cookie-parser": "1.4.9",
|
||||||
"@types/cors": "2.8.19",
|
"@types/cors": "2.8.19",
|
||||||
"@types/crypto-js": "^4.2.2",
|
"@types/crypto-js": "^4.2.2",
|
||||||
"@types/express": "5.0.3",
|
"@types/express": "5.0.0",
|
||||||
"@types/express-session": "^1.18.2",
|
|
||||||
"@types/jmespath": "^0.15.2",
|
"@types/jmespath": "^0.15.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.9",
|
||||||
"@types/node": "^24",
|
"@types/node": "^22",
|
||||||
"@types/nodemailer": "6.4.17",
|
"@types/nodemailer": "6.4.17",
|
||||||
"@types/pg": "8.15.5",
|
"@types/react": "19.1.7",
|
||||||
"@types/react": "19.1.12",
|
"@types/react-dom": "19.1.6",
|
||||||
"@types/react-dom": "19.1.9",
|
"@types/semver": "7.7.0",
|
||||||
"@types/semver": "^7.7.0",
|
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
"@types/swagger-ui-express": "^4.1.8",
|
||||||
"@types/ws": "8.18.1",
|
"@types/ws": "8.18.1",
|
||||||
"@types/yargs": "17.0.33",
|
"@types/yargs": "17.0.33",
|
||||||
"drizzle-kit": "0.31.4",
|
"drizzle-kit": "0.31.1",
|
||||||
"esbuild": "0.25.9",
|
"esbuild": "0.25.5",
|
||||||
"esbuild-node-externals": "1.18.0",
|
"esbuild-node-externals": "1.18.0",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"react-email": "4.2.8",
|
"react-email": "4.0.16",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
"tsc-alias": "1.8.16",
|
"tsc-alias": "1.8.16",
|
||||||
"tsx": "4.20.5",
|
"tsx": "4.19.4",
|
||||||
"typescript": "^5",
|
"typescript": "^5",
|
||||||
"typescript-eslint": "^8.40.0"
|
"typescript-eslint": "^8.34.0",
|
||||||
|
"yargs": "18.0.0"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
"emblor": {
|
"emblor": {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 647 KiB |
BIN
public/clip.gif
Before Width: | Height: | Size: 500 KiB |
Before Width: | Height: | Size: 52 KiB |
Before Width: | Height: | Size: 52 KiB |
BIN
public/screenshots/collage.png
Normal file
After Width: | Height: | Size: 574 KiB |
Before Width: | Height: | Size: 748 KiB |
Before Width: | Height: | Size: 688 KiB |
Before Width: | Height: | Size: 687 KiB |
Before Width: | Height: | Size: 669 KiB |
Before Width: | Height: | Size: 713 KiB |
Before Width: | Height: | Size: 636 KiB |
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 434 KiB |
Before Width: | Height: | Size: 356 KiB |
Before Width: | Height: | Size: 707 KiB |
Before Width: | Height: | Size: 713 KiB |
Before Width: | Height: | Size: 556 KiB |
Before Width: | Height: | Size: 585 KiB |
Before Width: | Height: | Size: 456 KiB |
Before Width: | Height: | Size: 674 KiB |
Before Width: | Height: | Size: 597 KiB |
|
@ -5,29 +5,23 @@ import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
errorHandlerMiddleware,
|
errorHandlerMiddleware,
|
||||||
notFoundMiddleware
|
notFoundMiddleware,
|
||||||
|
rateLimitMiddleware
|
||||||
} from "@server/middlewares";
|
} from "@server/middlewares";
|
||||||
import { authenticated, unauthenticated } from "@server/routers/external";
|
import { authenticated, unauthenticated } from "@server/routers/external";
|
||||||
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
import { router as wsRouter, handleWSUpgrade } from "@server/routers/ws";
|
||||||
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
import { logIncomingMiddleware } from "./middlewares/logIncoming";
|
||||||
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
import { csrfProtectionMiddleware } from "./middlewares/csrfProtection";
|
||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import HttpCode from "./types/HttpCode";
|
|
||||||
import requestTimeoutMiddleware from "./middlewares/requestTimeout";
|
|
||||||
import { createStore } from "./lib/rateLimitStore";
|
|
||||||
|
|
||||||
const dev = config.isDev;
|
const dev = config.isDev;
|
||||||
const externalPort = config.getRawConfig().server.external_port;
|
const externalPort = config.getRawConfig().server.external_port;
|
||||||
|
|
||||||
export function createApiServer() {
|
export function createApiServer() {
|
||||||
const apiServer = express();
|
const apiServer = express();
|
||||||
const prefix = `/api/v1`;
|
|
||||||
|
|
||||||
const trustProxy = config.getRawConfig().server.trust_proxy;
|
if (config.getRawConfig().server.trust_proxy) {
|
||||||
if (trustProxy) {
|
apiServer.set("trust proxy", 1);
|
||||||
apiServer.set("trust proxy", trustProxy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
@ -59,30 +53,19 @@ export function createApiServer() {
|
||||||
apiServer.use(cookieParser());
|
apiServer.use(cookieParser());
|
||||||
apiServer.use(express.json());
|
apiServer.use(express.json());
|
||||||
|
|
||||||
// Add request timeout middleware
|
|
||||||
apiServer.use(requestTimeoutMiddleware(60000)); // 60 second timeout
|
|
||||||
|
|
||||||
if (!dev) {
|
if (!dev) {
|
||||||
apiServer.use(
|
apiServer.use(
|
||||||
rateLimit({
|
rateLimitMiddleware({
|
||||||
windowMs:
|
windowMin:
|
||||||
config.getRawConfig().rate_limits.global.window_minutes *
|
config.getRawConfig().rate_limits.global.window_minutes,
|
||||||
60 *
|
|
||||||
1000,
|
|
||||||
max: config.getRawConfig().rate_limits.global.max_requests,
|
max: config.getRawConfig().rate_limits.global.max_requests,
|
||||||
keyGenerator: (req) => `apiServerGlobal:${ipKeyGenerator(req.ip || "")}:${req.path}`,
|
type: "IP_AND_PATH"
|
||||||
handler: (req, res, next) => {
|
|
||||||
const message = `Rate limit exceeded. You can make ${config.getRawConfig().rate_limits.global.max_requests} requests every ${config.getRawConfig().rate_limits.global.window_minutes} minute(s).`;
|
|
||||||
return next(
|
|
||||||
createHttpError(HttpCode.TOO_MANY_REQUESTS, message)
|
|
||||||
);
|
|
||||||
},
|
|
||||||
store: createStore()
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
|
const prefix = `/api/v1`;
|
||||||
apiServer.use(logIncomingMiddleware);
|
apiServer.use(logIncomingMiddleware);
|
||||||
apiServer.use(prefix, unauthenticated);
|
apiServer.use(prefix, unauthenticated);
|
||||||
apiServer.use(prefix, authenticated);
|
apiServer.use(prefix, authenticated);
|
||||||
|
|
|
@ -56,8 +56,6 @@ export enum ActionsEnum {
|
||||||
// removeUserAction = "removeUserAction",
|
// removeUserAction = "removeUserAction",
|
||||||
// removeUserSite = "removeUserSite",
|
// removeUserSite = "removeUserSite",
|
||||||
getOrgUser = "getOrgUser",
|
getOrgUser = "getOrgUser",
|
||||||
updateUser = "updateUser",
|
|
||||||
getUser = "getUser",
|
|
||||||
setResourcePassword = "setResourcePassword",
|
setResourcePassword = "setResourcePassword",
|
||||||
setResourcePincode = "setResourcePincode",
|
setResourcePincode = "setResourcePincode",
|
||||||
setResourceWhitelist = "setResourceWhitelist",
|
setResourceWhitelist = "setResourceWhitelist",
|
||||||
|
@ -69,16 +67,6 @@ export enum ActionsEnum {
|
||||||
deleteResourceRule = "deleteResourceRule",
|
deleteResourceRule = "deleteResourceRule",
|
||||||
listResourceRules = "listResourceRules",
|
listResourceRules = "listResourceRules",
|
||||||
updateResourceRule = "updateResourceRule",
|
updateResourceRule = "updateResourceRule",
|
||||||
createSiteResource = "createSiteResource",
|
|
||||||
deleteSiteResource = "deleteSiteResource",
|
|
||||||
getSiteResource = "getSiteResource",
|
|
||||||
listSiteResources = "listSiteResources",
|
|
||||||
updateSiteResource = "updateSiteResource",
|
|
||||||
createClient = "createClient",
|
|
||||||
deleteClient = "deleteClient",
|
|
||||||
updateClient = "updateClient",
|
|
||||||
listClients = "listClients",
|
|
||||||
getClient = "getClient",
|
|
||||||
listOrgDomains = "listOrgDomains",
|
listOrgDomains = "listOrgDomains",
|
||||||
createNewt = "createNewt",
|
createNewt = "createNewt",
|
||||||
createIdp = "createIdp",
|
createIdp = "createIdp",
|
||||||
|
@ -97,10 +85,7 @@ export enum ActionsEnum {
|
||||||
setApiKeyOrgs = "setApiKeyOrgs",
|
setApiKeyOrgs = "setApiKeyOrgs",
|
||||||
listApiKeyActions = "listApiKeyActions",
|
listApiKeyActions = "listApiKeyActions",
|
||||||
listApiKeys = "listApiKeys",
|
listApiKeys = "listApiKeys",
|
||||||
getApiKey = "getApiKey",
|
getApiKey = "getApiKey"
|
||||||
createOrgDomain = "createOrgDomain",
|
|
||||||
deleteOrgDomain = "deleteOrgDomain",
|
|
||||||
restartOrgDomain = "restartOrgDomain"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkUserActionPermission(
|
export async function checkUserActionPermission(
|
||||||
|
|
40
server/auth/limits.ts
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
import { db } from '@server/db';
|
||||||
|
import { limitsTable } from '@server/db';
|
||||||
|
import { and, eq } from 'drizzle-orm';
|
||||||
|
import createHttpError from 'http-errors';
|
||||||
|
import HttpCode from '@server/types/HttpCode';
|
||||||
|
|
||||||
|
interface CheckLimitOptions {
|
||||||
|
orgId: string;
|
||||||
|
limitName: string;
|
||||||
|
currentValue: number;
|
||||||
|
increment?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function checkOrgLimit({ orgId, limitName, currentValue, increment = 0 }: CheckLimitOptions): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const limit = await db.select()
|
||||||
|
.from(limitsTable)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(limitsTable.orgId, orgId),
|
||||||
|
eq(limitsTable.name, limitName)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (limit.length === 0) {
|
||||||
|
throw createHttpError(HttpCode.NOT_FOUND, `Limit "${limitName}" not found for organization`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limitValue = limit[0].value;
|
||||||
|
|
||||||
|
// Check if the current value plus the increment is within the limit
|
||||||
|
return (currentValue + increment) <= limitValue;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, `Error checking limit: ${error.message}`);
|
||||||
|
}
|
||||||
|
throw createHttpError(HttpCode.INTERNAL_SERVER_ERROR, 'Unknown error occurred while checking limit');
|
||||||
|
}
|
||||||
|
}
|
|
@ -24,8 +24,8 @@ export const SESSION_COOKIE_EXPIRES =
|
||||||
60 *
|
60 *
|
||||||
60 *
|
60 *
|
||||||
config.getRawConfig().server.dashboard_session_length_hours;
|
config.getRawConfig().server.dashboard_session_length_hours;
|
||||||
export const COOKIE_DOMAIN = config.getRawConfig().app.dashboard_url ?
|
export const COOKIE_DOMAIN =
|
||||||
"." + new URL(config.getRawConfig().app.dashboard_url!).hostname : undefined;
|
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
||||||
|
|
||||||
export function generateSessionToken(): string {
|
export function generateSessionToken(): string {
|
||||||
const bytes = new Uint8Array(20);
|
const bytes = new Uint8Array(20);
|
||||||
|
|
|
@ -1,72 +0,0 @@
|
||||||
import {
|
|
||||||
encodeHexLowerCase,
|
|
||||||
} from "@oslojs/encoding";
|
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
|
||||||
import { Olm, olms, olmSessions, OlmSession } from "@server/db";
|
|
||||||
import { db } from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
export const EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
|
||||||
|
|
||||||
export async function createOlmSession(
|
|
||||||
token: string,
|
|
||||||
olmId: string,
|
|
||||||
): Promise<OlmSession> {
|
|
||||||
const sessionId = encodeHexLowerCase(
|
|
||||||
sha256(new TextEncoder().encode(token)),
|
|
||||||
);
|
|
||||||
const session: OlmSession = {
|
|
||||||
sessionId: sessionId,
|
|
||||||
olmId,
|
|
||||||
expiresAt: new Date(Date.now() + EXPIRES).getTime(),
|
|
||||||
};
|
|
||||||
await db.insert(olmSessions).values(session);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateOlmSessionToken(
|
|
||||||
token: string,
|
|
||||||
): Promise<SessionValidationResult> {
|
|
||||||
const sessionId = encodeHexLowerCase(
|
|
||||||
sha256(new TextEncoder().encode(token)),
|
|
||||||
);
|
|
||||||
const result = await db
|
|
||||||
.select({ olm: olms, session: olmSessions })
|
|
||||||
.from(olmSessions)
|
|
||||||
.innerJoin(olms, eq(olmSessions.olmId, olms.olmId))
|
|
||||||
.where(eq(olmSessions.sessionId, sessionId));
|
|
||||||
if (result.length < 1) {
|
|
||||||
return { session: null, olm: null };
|
|
||||||
}
|
|
||||||
const { olm, session } = result[0];
|
|
||||||
if (Date.now() >= session.expiresAt) {
|
|
||||||
await db
|
|
||||||
.delete(olmSessions)
|
|
||||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
|
||||||
return { session: null, olm: null };
|
|
||||||
}
|
|
||||||
if (Date.now() >= session.expiresAt - (EXPIRES / 2)) {
|
|
||||||
session.expiresAt = new Date(
|
|
||||||
Date.now() + EXPIRES,
|
|
||||||
).getTime();
|
|
||||||
await db
|
|
||||||
.update(olmSessions)
|
|
||||||
.set({
|
|
||||||
expiresAt: session.expiresAt,
|
|
||||||
})
|
|
||||||
.where(eq(olmSessions.sessionId, session.sessionId));
|
|
||||||
}
|
|
||||||
return { session, olm };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invalidateOlmSession(sessionId: string): Promise<void> {
|
|
||||||
await db.delete(olmSessions).where(eq(olmSessions.sessionId, sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invalidateAllOlmSessions(olmId: string): Promise<void> {
|
|
||||||
await db.delete(olmSessions).where(eq(olmSessions.olmId, olmId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionValidationResult =
|
|
||||||
| { session: OlmSession; olm: Olm }
|
|
||||||
| { session: null; olm: null };
|
|
|
@ -4,9 +4,6 @@ import { resourceSessions, ResourceSession } from "@server/db";
|
||||||
import { db } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import axios from "axios";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME =
|
export const SESSION_COOKIE_NAME =
|
||||||
config.getRawConfig().server.session_cookie_name;
|
config.getRawConfig().server.session_cookie_name;
|
||||||
|
@ -65,29 +62,6 @@ export async function validateResourceSessionToken(
|
||||||
token: string,
|
token: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceSessionValidationResult> {
|
): Promise<ResourceSessionValidationResult> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.post(`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/session/validate`, {
|
|
||||||
token: token
|
|
||||||
}, await tokenManager.getAuthHeader());
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", {
|
|
||||||
message: error.message,
|
|
||||||
code: error.code,
|
|
||||||
status: error.response?.status,
|
|
||||||
statusText: error.response?.statusText,
|
|
||||||
url: error.config?.url,
|
|
||||||
method: error.config?.method
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
logger.error("Error validating resource session token in hybrid mode:", error);
|
|
||||||
}
|
|
||||||
return { resourceSession: null };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sessionId = encodeHexLowerCase(
|
const sessionId = encodeHexLowerCase(
|
||||||
sha256(new TextEncoder().encode(token))
|
sha256(new TextEncoder().encode(token))
|
||||||
);
|
);
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
export const build = "oss" as any;
|
|
2
server/db/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./sqlite";
|
||||||
|
// export * from "./pg";
|
|
@ -59,7 +59,7 @@ export async function getUniqueExitNodeEndpointName(): Promise<string> {
|
||||||
|
|
||||||
|
|
||||||
export function generateName(): string {
|
export function generateName(): string {
|
||||||
const name = (
|
return (
|
||||||
names.descriptors[
|
names.descriptors[
|
||||||
Math.floor(Math.random() * names.descriptors.length)
|
Math.floor(Math.random() * names.descriptors.length)
|
||||||
] +
|
] +
|
||||||
|
@ -68,7 +68,4 @@ export function generateName(): string {
|
||||||
)
|
)
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.replace(/\s/g, "-");
|
.replace(/\s/g, "-");
|
||||||
|
|
||||||
// clean out any non-alphanumeric characters except for dashes
|
|
||||||
return name.replace(/[^a-z0-9-]/g, "");
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,51 +1,16 @@
|
||||||
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres";
|
||||||
import { Pool } from "pg";
|
|
||||||
import { readConfigFile } from "@server/lib/readConfigFile";
|
import { readConfigFile } from "@server/lib/readConfigFile";
|
||||||
import { withReplicas } from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
function createDb() {
|
function createDb() {
|
||||||
const config = readConfigFile();
|
const config = readConfigFile();
|
||||||
|
|
||||||
if (!config.postgres) {
|
|
||||||
throw new Error(
|
|
||||||
"Postgres configuration is missing in the configuration file."
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const connectionString = config.postgres?.connection_string;
|
const connectionString = config.postgres?.connection_string;
|
||||||
const replicaConnections = config.postgres?.replicas || [];
|
|
||||||
|
|
||||||
if (!connectionString) {
|
if (!connectionString) {
|
||||||
throw new Error(
|
throw new Error("Postgres connection string is not defined in the configuration file.");
|
||||||
"A primary db connection string is required in the configuration file."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create connection pools instead of individual connections
|
return DrizzlePostgres(connectionString);
|
||||||
const primaryPool = new Pool({
|
|
||||||
connectionString,
|
|
||||||
max: 20,
|
|
||||||
idleTimeoutMillis: 30000,
|
|
||||||
connectionTimeoutMillis: 2000,
|
|
||||||
});
|
|
||||||
|
|
||||||
const replicas = [];
|
|
||||||
|
|
||||||
if (!replicaConnections.length) {
|
|
||||||
replicas.push(DrizzlePostgres(primaryPool));
|
|
||||||
} else {
|
|
||||||
for (const conn of replicaConnections) {
|
|
||||||
const replicaPool = new Pool({
|
|
||||||
connectionString: conn.connection_string,
|
|
||||||
max: 10,
|
|
||||||
idleTimeoutMillis: 30000,
|
|
||||||
connectionTimeoutMillis: 2000,
|
|
||||||
});
|
|
||||||
replicas.push(DrizzlePostgres(replicaPool));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return withReplicas(DrizzlePostgres(primaryPool), replicas as any);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const db = createDb();
|
export const db = createDb();
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
import { migrate } from "drizzle-orm/node-postgres/migrator";
|
||||||
import { db } from "./driver";
|
import db from "./driver";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
const migrationsFolder = path.join("server/migrations");
|
const migrationsFolder = path.join("server/migrations");
|
||||||
|
|
|
@ -5,26 +5,19 @@ import {
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
bigint,
|
bigint,
|
||||||
real,
|
real
|
||||||
text
|
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import { InferSelectModel } from "drizzle-orm";
|
import { InferSelectModel } from "drizzle-orm";
|
||||||
|
|
||||||
export const domains = pgTable("domains", {
|
export const domains = pgTable("domains", {
|
||||||
domainId: varchar("domainId").primaryKey(),
|
domainId: varchar("domainId").primaryKey(),
|
||||||
baseDomain: varchar("baseDomain").notNull(),
|
baseDomain: varchar("baseDomain").notNull(),
|
||||||
configManaged: boolean("configManaged").notNull().default(false),
|
configManaged: boolean("configManaged").notNull().default(false)
|
||||||
type: varchar("type"), // "ns", "cname", "wildcard"
|
|
||||||
verified: boolean("verified").notNull().default(false),
|
|
||||||
failed: boolean("failed").notNull().default(false),
|
|
||||||
tries: integer("tries").notNull().default(0)
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgs = pgTable("orgs", {
|
export const orgs = pgTable("orgs", {
|
||||||
orgId: varchar("orgId").primaryKey(),
|
orgId: varchar("orgId").primaryKey(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull()
|
||||||
subnet: varchar("subnet"),
|
|
||||||
createdAt: text("createdAt")
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
|
@ -49,23 +42,22 @@ export const sites = pgTable("sites", {
|
||||||
}),
|
}),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet").notNull(),
|
||||||
megabytesIn: real("bytesIn").default(0),
|
megabytesIn: real("bytesIn"),
|
||||||
megabytesOut: real("bytesOut").default(0),
|
megabytesOut: real("bytesOut"),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
type: varchar("type").notNull(), // "newt" or "wireguard"
|
type: varchar("type").notNull(), // "newt" or "wireguard"
|
||||||
online: boolean("online").notNull().default(false),
|
online: boolean("online").notNull().default(false),
|
||||||
address: varchar("address"),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true)
|
||||||
endpoint: varchar("endpoint"),
|
|
||||||
publicKey: varchar("publicKey"),
|
|
||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
|
||||||
listenPort: integer("listenPort"),
|
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
|
||||||
remoteSubnets: text("remoteSubnets") // comma-separated list of subnets that this site can access
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const resources = pgTable("resources", {
|
export const resources = pgTable("resources", {
|
||||||
resourceId: serial("resourceId").primaryKey(),
|
resourceId: serial("resourceId").primaryKey(),
|
||||||
|
siteId: integer("siteId")
|
||||||
|
.references(() => sites.siteId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
.references(() => orgs.orgId, {
|
.references(() => orgs.orgId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
|
@ -86,15 +78,12 @@ export const resources = pgTable("resources", {
|
||||||
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
emailWhitelistEnabled: boolean("emailWhitelistEnabled")
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false),
|
.default(false),
|
||||||
|
isBaseDomain: boolean("isBaseDomain"),
|
||||||
applyRules: boolean("applyRules").notNull().default(false),
|
applyRules: boolean("applyRules").notNull().default(false),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
stickySession: boolean("stickySession").notNull().default(false),
|
stickySession: boolean("stickySession").notNull().default(false),
|
||||||
tlsServerName: varchar("tlsServerName"),
|
tlsServerName: varchar("tlsServerName"),
|
||||||
setHostHeader: varchar("setHostHeader"),
|
setHostHeader: varchar("setHostHeader")
|
||||||
enableProxy: boolean("enableProxy").default(true),
|
|
||||||
skipToIdpId: integer("skipToIdpId").references(() => idp.idpId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = pgTable("targets", {
|
export const targets = pgTable("targets", {
|
||||||
|
@ -104,11 +93,6 @@ export const targets = pgTable("targets", {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
.notNull(),
|
.notNull(),
|
||||||
siteId: integer("siteId")
|
|
||||||
.references(() => sites.siteId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
ip: varchar("ip").notNull(),
|
ip: varchar("ip").notNull(),
|
||||||
method: varchar("method"),
|
method: varchar("method"),
|
||||||
port: integer("port").notNull(),
|
port: integer("port").notNull(),
|
||||||
|
@ -123,27 +107,7 @@ export const exitNodes = pgTable("exitNodes", {
|
||||||
endpoint: varchar("endpoint").notNull(),
|
endpoint: varchar("endpoint").notNull(),
|
||||||
publicKey: varchar("publicKey").notNull(),
|
publicKey: varchar("publicKey").notNull(),
|
||||||
listenPort: integer("listenPort").notNull(),
|
listenPort: integer("listenPort").notNull(),
|
||||||
reachableAt: varchar("reachableAt"),
|
reachableAt: varchar("reachableAt")
|
||||||
maxConnections: integer("maxConnections"),
|
|
||||||
online: boolean("online").notNull().default(false),
|
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
type: text("type").default("gerbil") // gerbil, remoteExitNode
|
|
||||||
});
|
|
||||||
|
|
||||||
export const siteResources = pgTable("siteResources", { // this is for the clients
|
|
||||||
siteResourceId: serial("siteResourceId").primaryKey(),
|
|
||||||
siteId: integer("siteId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
|
||||||
orgId: varchar("orgId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => orgs.orgId, { onDelete: "cascade" }),
|
|
||||||
name: varchar("name").notNull(),
|
|
||||||
protocol: varchar("protocol").notNull(),
|
|
||||||
proxyPort: integer("proxyPort").notNull(),
|
|
||||||
destinationPort: integer("destinationPort").notNull(),
|
|
||||||
destinationIp: varchar("destinationIp").notNull(),
|
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
export const users = pgTable("user", {
|
||||||
|
@ -157,12 +121,9 @@ export const users = pgTable("user", {
|
||||||
}),
|
}),
|
||||||
passwordHash: varchar("passwordHash"),
|
passwordHash: varchar("passwordHash"),
|
||||||
twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false),
|
twoFactorEnabled: boolean("twoFactorEnabled").notNull().default(false),
|
||||||
twoFactorSetupRequested: boolean("twoFactorSetupRequested").default(false),
|
|
||||||
twoFactorSecret: varchar("twoFactorSecret"),
|
twoFactorSecret: varchar("twoFactorSecret"),
|
||||||
emailVerified: boolean("emailVerified").notNull().default(false),
|
emailVerified: boolean("emailVerified").notNull().default(false),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"),
|
|
||||||
termsVersion: varchar("termsVersion"),
|
|
||||||
serverAdmin: boolean("serverAdmin").notNull().default(false)
|
serverAdmin: boolean("serverAdmin").notNull().default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -170,7 +131,6 @@ export const newts = pgTable("newt", {
|
||||||
newtId: varchar("id").primaryKey(),
|
newtId: varchar("id").primaryKey(),
|
||||||
secretHash: varchar("secretHash").notNull(),
|
secretHash: varchar("secretHash").notNull(),
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
dateCreated: varchar("dateCreated").notNull(),
|
||||||
version: varchar("version"),
|
|
||||||
siteId: integer("siteId").references(() => sites.siteId, {
|
siteId: integer("siteId").references(() => sites.siteId, {
|
||||||
onDelete: "cascade"
|
onDelete: "cascade"
|
||||||
})
|
})
|
||||||
|
@ -313,6 +273,18 @@ export const userResources = pgTable("userResources", {
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" })
|
.references(() => resources.resourceId, { onDelete: "cascade" })
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const limitsTable = pgTable("limits", {
|
||||||
|
limitId: serial("limitId").primaryKey(),
|
||||||
|
orgId: varchar("orgId")
|
||||||
|
.references(() => orgs.orgId, {
|
||||||
|
onDelete: "cascade"
|
||||||
|
})
|
||||||
|
.notNull(),
|
||||||
|
name: varchar("name").notNull(),
|
||||||
|
value: bigint("value", { mode: "number" }).notNull(),
|
||||||
|
description: varchar("description")
|
||||||
|
});
|
||||||
|
|
||||||
export const userInvites = pgTable("userInvites", {
|
export const userInvites = pgTable("userInvites", {
|
||||||
inviteId: varchar("inviteId").primaryKey(),
|
inviteId: varchar("inviteId").primaryKey(),
|
||||||
orgId: varchar("orgId")
|
orgId: varchar("orgId")
|
||||||
|
@ -430,7 +402,7 @@ export const resourceRules = pgTable("resourceRules", {
|
||||||
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
.references(() => resources.resourceId, { onDelete: "cascade" }),
|
||||||
enabled: boolean("enabled").notNull().default(true),
|
enabled: boolean("enabled").notNull().default(true),
|
||||||
priority: integer("priority").notNull(),
|
priority: integer("priority").notNull(),
|
||||||
action: varchar("action").notNull(), // ACCEPT, DROP, PASS
|
action: varchar("action").notNull(), // ACCEPT, DROP
|
||||||
match: varchar("match").notNull(), // CIDR, PATH, IP
|
match: varchar("match").notNull(), // CIDR, PATH, IP
|
||||||
value: varchar("value").notNull()
|
value: varchar("value").notNull()
|
||||||
});
|
});
|
||||||
|
@ -519,111 +491,6 @@ export const idpOrg = pgTable("idpOrg", {
|
||||||
orgMapping: varchar("orgMapping")
|
orgMapping: varchar("orgMapping")
|
||||||
});
|
});
|
||||||
|
|
||||||
export const clients = pgTable("clients", {
|
|
||||||
clientId: serial("id").primaryKey(),
|
|
||||||
orgId: varchar("orgId")
|
|
||||||
.references(() => orgs.orgId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
.notNull(),
|
|
||||||
exitNodeId: integer("exitNode").references(() => exitNodes.exitNodeId, {
|
|
||||||
onDelete: "set null"
|
|
||||||
}),
|
|
||||||
name: varchar("name").notNull(),
|
|
||||||
pubKey: varchar("pubKey"),
|
|
||||||
subnet: varchar("subnet").notNull(),
|
|
||||||
megabytesIn: real("bytesIn"),
|
|
||||||
megabytesOut: real("bytesOut"),
|
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
|
||||||
lastPing: integer("lastPing"),
|
|
||||||
type: varchar("type").notNull(), // "olm"
|
|
||||||
online: boolean("online").notNull().default(false),
|
|
||||||
// endpoint: varchar("endpoint"),
|
|
||||||
lastHolePunch: integer("lastHolePunch"),
|
|
||||||
maxConnections: integer("maxConnections")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const clientSites = pgTable("clientSites", {
|
|
||||||
clientId: integer("clientId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" }),
|
|
||||||
siteId: integer("siteId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => sites.siteId, { onDelete: "cascade" }),
|
|
||||||
isRelayed: boolean("isRelayed").notNull().default(false),
|
|
||||||
endpoint: varchar("endpoint")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const olms = pgTable("olms", {
|
|
||||||
olmId: varchar("id").primaryKey(),
|
|
||||||
secretHash: varchar("secretHash").notNull(),
|
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
|
||||||
version: text("version"),
|
|
||||||
clientId: integer("clientId").references(() => clients.clientId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
export const olmSessions = pgTable("clientSession", {
|
|
||||||
sessionId: varchar("id").primaryKey(),
|
|
||||||
olmId: varchar("olmId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => olms.olmId, { onDelete: "cascade" }),
|
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const userClients = pgTable("userClients", {
|
|
||||||
userId: varchar("userId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.userId, { onDelete: "cascade" }),
|
|
||||||
clientId: integer("clientId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
|
||||||
});
|
|
||||||
|
|
||||||
export const roleClients = pgTable("roleClients", {
|
|
||||||
roleId: integer("roleId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => roles.roleId, { onDelete: "cascade" }),
|
|
||||||
clientId: integer("clientId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => clients.clientId, { onDelete: "cascade" })
|
|
||||||
});
|
|
||||||
|
|
||||||
export const securityKeys = pgTable("webauthnCredentials", {
|
|
||||||
credentialId: varchar("credentialId").primaryKey(),
|
|
||||||
userId: varchar("userId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.userId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
publicKey: varchar("publicKey").notNull(),
|
|
||||||
signCount: integer("signCount").notNull(),
|
|
||||||
transports: varchar("transports"),
|
|
||||||
name: varchar("name"),
|
|
||||||
lastUsed: varchar("lastUsed").notNull(),
|
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
|
||||||
securityKeyName: varchar("securityKeyName")
|
|
||||||
});
|
|
||||||
|
|
||||||
export const webauthnChallenge = pgTable("webauthnChallenge", {
|
|
||||||
sessionId: varchar("sessionId").primaryKey(),
|
|
||||||
challenge: varchar("challenge").notNull(),
|
|
||||||
securityKeyName: varchar("securityKeyName"),
|
|
||||||
userId: varchar("userId").references(() => users.userId, {
|
|
||||||
onDelete: "cascade"
|
|
||||||
}),
|
|
||||||
expiresAt: bigint("expiresAt", { mode: "number" }).notNull() // Unix timestamp
|
|
||||||
});
|
|
||||||
|
|
||||||
export const setupTokens = pgTable("setupTokens", {
|
|
||||||
tokenId: varchar("tokenId").primaryKey(),
|
|
||||||
token: varchar("token").notNull(),
|
|
||||||
used: boolean("used").notNull().default(false),
|
|
||||||
dateCreated: varchar("dateCreated").notNull(),
|
|
||||||
dateUsed: varchar("dateUsed")
|
|
||||||
});
|
|
||||||
|
|
||||||
export type Org = InferSelectModel<typeof orgs>;
|
export type Org = InferSelectModel<typeof orgs>;
|
||||||
export type User = InferSelectModel<typeof users>;
|
export type User = InferSelectModel<typeof users>;
|
||||||
export type Site = InferSelectModel<typeof sites>;
|
export type Site = InferSelectModel<typeof sites>;
|
||||||
|
@ -646,6 +513,7 @@ export type RoleSite = InferSelectModel<typeof roleSites>;
|
||||||
export type UserSite = InferSelectModel<typeof userSites>;
|
export type UserSite = InferSelectModel<typeof userSites>;
|
||||||
export type RoleResource = InferSelectModel<typeof roleResources>;
|
export type RoleResource = InferSelectModel<typeof roleResources>;
|
||||||
export type UserResource = InferSelectModel<typeof userResources>;
|
export type UserResource = InferSelectModel<typeof userResources>;
|
||||||
|
export type Limit = InferSelectModel<typeof limitsTable>;
|
||||||
export type UserInvite = InferSelectModel<typeof userInvites>;
|
export type UserInvite = InferSelectModel<typeof userInvites>;
|
||||||
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
export type UserOrg = InferSelectModel<typeof userOrgs>;
|
||||||
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
export type ResourceSession = InferSelectModel<typeof resourceSessions>;
|
||||||
|
@ -662,13 +530,3 @@ export type Idp = InferSelectModel<typeof idp>;
|
||||||
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
export type ApiKey = InferSelectModel<typeof apiKeys>;
|
||||||
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
|
||||||
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
|
||||||
export type Client = InferSelectModel<typeof clients>;
|
|
||||||
export type ClientSite = InferSelectModel<typeof clientSites>;
|
|
||||||
export type Olm = InferSelectModel<typeof olms>;
|
|
||||||
export type OlmSession = InferSelectModel<typeof olmSessions>;
|
|
||||||
export type UserClient = InferSelectModel<typeof userClients>;
|
|
||||||
export type RoleClient = InferSelectModel<typeof roleClients>;
|
|
||||||
export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
|
||||||
export type SiteResource = InferSelectModel<typeof siteResources>;
|
|
||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
|
||||||
|
|