mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-26 14:56:39 +00:00
Merge branch 'dev' into distribution
This commit is contained in:
75
.github/workflows/dev-image.yml
vendored
75
.github/workflows/dev-image.yml
vendored
@@ -1,75 +0,0 @@
|
|||||||
name: Create Dev-Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
types:
|
|
||||||
- opened
|
|
||||||
- synchronize
|
|
||||||
- reopened
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
docker:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
env:
|
|
||||||
TAG_URL: https://hub.docker.com/r/${{ vars.DOCKER_HUB_REPO }}/tags
|
|
||||||
TAG: ${{ vars.DOCKER_HUB_REPO }}:dev-pr${{ github.event.pull_request.number }}
|
|
||||||
TAG_PG: ${{ vars.DOCKER_HUB_REPO }}:postgresql-dev-pr${{ github.event.pull_request.number }}
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Build and push Docker image SQLITE
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.TAG }}
|
|
||||||
cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache
|
|
||||||
cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache,mode=max
|
|
||||||
build-args: DATABASE=sqlite
|
|
||||||
|
|
||||||
- name: Build and push Docker image PG
|
|
||||||
uses: docker/build-push-action@v6
|
|
||||||
with:
|
|
||||||
platforms: linux/amd64
|
|
||||||
push: true
|
|
||||||
tags: ${{ env.TAG_PG }}
|
|
||||||
cache-from: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg
|
|
||||||
cache-to: type=registry,ref=${{ vars.DOCKER_HUB_REPO }}:buildcache-pg,mode=max
|
|
||||||
build-args: DATABASE=pg
|
|
||||||
|
|
||||||
- uses: actions/github-script@v8
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
const repoUrl = process.env.TAG_URL;
|
|
||||||
const tag = process.env.TAG;
|
|
||||||
const tagPg = process.env.TAG_PG;
|
|
||||||
github.rest.issues.createComment({
|
|
||||||
issue_number: context.issue.number,
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
body: `👋 Thanks for your PR!
|
|
||||||
Dev images for this PR are now available on [docker hub](${repoUrl}):
|
|
||||||
|
|
||||||
**SQLITE Image:**
|
|
||||||
\`\`\`
|
|
||||||
${tag}
|
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
**Postgresql Image:**
|
|
||||||
\`\`\`
|
|
||||||
${tagPg}
|
|
||||||
\`\`\``
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1541,8 +1541,8 @@
|
|||||||
"autoLoginError": "Auto Login Error",
|
"autoLoginError": "Auto Login Error",
|
||||||
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
"autoLoginErrorNoRedirectUrl": "No redirect URL received from the identity provider.",
|
||||||
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
"autoLoginErrorGeneratingUrl": "Failed to generate authentication URL.",
|
||||||
"remoteExitNodeManageRemoteExitNodes": "Managed Nodes",
|
"remoteExitNodeManageRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeDescription": "Self-host one or more nodes for tunnel exit servers",
|
"remoteExitNodeDescription": "Self-host one or more remote nodes for tunnel exit servers",
|
||||||
"remoteExitNodes": "Nodes",
|
"remoteExitNodes": "Nodes",
|
||||||
"searchRemoteExitNodes": "Search nodes...",
|
"searchRemoteExitNodes": "Search nodes...",
|
||||||
"remoteExitNodeAdd": "Add Node",
|
"remoteExitNodeAdd": "Add Node",
|
||||||
@@ -1552,7 +1552,7 @@
|
|||||||
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
"remoteExitNodeMessageConfirm": "To confirm, please type the name of the node below.",
|
||||||
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
"remoteExitNodeConfirmDelete": "Confirm Delete Node",
|
||||||
"remoteExitNodeDelete": "Delete Node",
|
"remoteExitNodeDelete": "Delete Node",
|
||||||
"sidebarRemoteExitNodes": "Nodes",
|
"sidebarRemoteExitNodes": "Remote Nodes",
|
||||||
"remoteExitNodeCreate": {
|
"remoteExitNodeCreate": {
|
||||||
"title": "Create Node",
|
"title": "Create Node",
|
||||||
"description": "Create a new node to extend your network connectivity",
|
"description": "Create a new node to extend your network connectivity",
|
||||||
|
|||||||
@@ -44,27 +44,25 @@ export function createApiServer() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const corsConfig = config.getRawConfig().server.cors;
|
const corsConfig = config.getRawConfig().server.cors;
|
||||||
|
const options = {
|
||||||
|
...(corsConfig?.origins
|
||||||
|
? { origin: corsConfig.origins }
|
||||||
|
: {
|
||||||
|
origin: (origin: any, callback: any) => {
|
||||||
|
callback(null, true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
||||||
|
...(corsConfig?.allowed_headers && {
|
||||||
|
allowedHeaders: corsConfig.allowed_headers
|
||||||
|
}),
|
||||||
|
credentials: !(corsConfig?.credentials === false)
|
||||||
|
};
|
||||||
|
|
||||||
if (build == "oss") {
|
if (build == "oss" || !corsConfig) {
|
||||||
const options = {
|
|
||||||
...(corsConfig?.origins
|
|
||||||
? { origin: corsConfig.origins }
|
|
||||||
: {
|
|
||||||
origin: (origin: any, callback: any) => {
|
|
||||||
callback(null, true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
...(corsConfig?.methods && { methods: corsConfig.methods }),
|
|
||||||
...(corsConfig?.allowed_headers && {
|
|
||||||
allowedHeaders: corsConfig.allowed_headers
|
|
||||||
}),
|
|
||||||
credentials: !(corsConfig?.credentials === false)
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Using CORS options", options);
|
logger.debug("Using CORS options", options);
|
||||||
|
|
||||||
apiServer.use(cors(options));
|
apiServer.use(cors(options));
|
||||||
} else {
|
} else if (corsConfig) {
|
||||||
// Use the custom CORS middleware with loginPage support
|
// Use the custom CORS middleware with loginPage support
|
||||||
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
apiServer.use(corsWithLoginPageSupport(corsConfig));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -721,3 +721,4 @@ export type SiteResource = InferSelectModel<typeof siteResources>;
|
|||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
|
|||||||
@@ -17,10 +17,6 @@ import {
|
|||||||
users
|
users
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq } from "drizzle-orm";
|
import { and, eq } from "drizzle-orm";
|
||||||
import axios from "axios";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
|
|
||||||
export type ResourceWithAuth = {
|
export type ResourceWithAuth = {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
@@ -40,30 +36,6 @@ export type UserSessionWithUser = {
|
|||||||
export async function getResourceByDomain(
|
export async function getResourceByDomain(
|
||||||
domain: string
|
domain: string
|
||||||
): Promise<ResourceWithAuth | null> {
|
): Promise<ResourceWithAuth | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/domain/${domain}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resources)
|
.from(resources)
|
||||||
@@ -100,30 +72,6 @@ export async function getResourceByDomain(
|
|||||||
export async function getUserSessionWithUser(
|
export async function getUserSessionWithUser(
|
||||||
userSessionId: string
|
userSessionId: string
|
||||||
): Promise<UserSessionWithUser | null> {
|
): Promise<UserSessionWithUser | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/session/${userSessionId}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [res] = await db
|
const [res] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sessions)
|
.from(sessions)
|
||||||
@@ -144,30 +92,6 @@ export async function getUserSessionWithUser(
|
|||||||
* Get user organization role
|
* Get user organization role
|
||||||
*/
|
*/
|
||||||
export async function getUserOrgRole(userId: string, orgId: string) {
|
export async function getUserOrgRole(userId: string, orgId: string) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/org/${orgId}/role`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userOrgRole = await db
|
const userOrgRole = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userOrgs)
|
.from(userOrgs)
|
||||||
@@ -184,30 +108,6 @@ export async function getRoleResourceAccess(
|
|||||||
resourceId: number,
|
resourceId: number,
|
||||||
roleId: number
|
roleId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/role/${roleId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const roleResourceAccess = await db
|
const roleResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(roleResources)
|
.from(roleResources)
|
||||||
@@ -229,30 +129,6 @@ export async function getUserResourceAccess(
|
|||||||
userId: string,
|
userId: string,
|
||||||
resourceId: number
|
resourceId: number
|
||||||
) {
|
) {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/user/${userId}/resource/${resourceId}/access`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const userResourceAccess = await db
|
const userResourceAccess = await db
|
||||||
.select()
|
.select()
|
||||||
.from(userResources)
|
.from(userResources)
|
||||||
@@ -273,30 +149,6 @@ export async function getUserResourceAccess(
|
|||||||
export async function getResourceRules(
|
export async function getResourceRules(
|
||||||
resourceId: number
|
resourceId: number
|
||||||
): Promise<ResourceRule[]> {
|
): Promise<ResourceRule[]> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/resource/${resourceId}/rules`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const rules = await db
|
const rules = await db
|
||||||
.select()
|
.select()
|
||||||
.from(resourceRules)
|
.from(resourceRules)
|
||||||
@@ -311,30 +163,6 @@ export async function getResourceRules(
|
|||||||
export async function getOrgLoginPage(
|
export async function getOrgLoginPage(
|
||||||
orgId: string
|
orgId: string
|
||||||
): Promise<LoginPage | null> {
|
): Promise<LoginPage | null> {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/org/${orgId}/login-page`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const [result] = await db
|
const [result] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(loginPageOrg)
|
.from(loginPageOrg)
|
||||||
|
|||||||
@@ -760,3 +760,4 @@ export type OrgDomains = InferSelectModel<typeof orgDomains>;
|
|||||||
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
export type SetupToken = InferSelectModel<typeof setupTokens>;
|
||||||
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
export type HostMeta = InferSelectModel<typeof hostMeta>;
|
||||||
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
|
||||||
|
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
|
||||||
|
|||||||
@@ -6,11 +6,6 @@ import logger from "@server/logger";
|
|||||||
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
import SMTPTransport from "nodemailer/lib/smtp-transport";
|
||||||
|
|
||||||
function createEmailClient() {
|
function createEmailClient() {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
// LETS NOT WORRY ABOUT EMAILS IN HYBRID
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emailConfig = config.getRawConfig().email;
|
const emailConfig = config.getRawConfig().email;
|
||||||
if (!emailConfig) {
|
if (!emailConfig) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
import logger from "@server/logger";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { createWebSocketClient } from "./routers/ws/client";
|
|
||||||
import { addPeer, deletePeer } from "./routers/gerbil/peers";
|
|
||||||
import { db, exitNodes } from "./db";
|
|
||||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager";
|
|
||||||
import { tokenManager } from "./lib/tokenManager";
|
|
||||||
import { APP_VERSION } from "./lib/consts";
|
|
||||||
import axios from "axios";
|
|
||||||
|
|
||||||
export async function createHybridClientServer() {
|
|
||||||
logger.info("Starting hybrid client server...");
|
|
||||||
|
|
||||||
// Start the token manager
|
|
||||||
await tokenManager.start();
|
|
||||||
|
|
||||||
const token = await tokenManager.getToken();
|
|
||||||
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
|
|
||||||
await monitor.start();
|
|
||||||
|
|
||||||
// Create client
|
|
||||||
const client = createWebSocketClient(
|
|
||||||
token,
|
|
||||||
config.getRawConfig().managed!.endpoint!,
|
|
||||||
{
|
|
||||||
reconnectInterval: 5000,
|
|
||||||
pingInterval: 30000,
|
|
||||||
pingTimeout: 10000
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// Register message handlers
|
|
||||||
client.registerHandler("remoteExitNode/peers/add", async (message) => {
|
|
||||||
const { publicKey, allowedIps } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await addPeer(exitNode.exitNodeId, {
|
|
||||||
publicKey: publicKey,
|
|
||||||
allowedIps: allowedIps || []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/peers/remove", async (message) => {
|
|
||||||
const { publicKey } = message.data;
|
|
||||||
|
|
||||||
// TODO: we are getting the exit node twice here
|
|
||||||
// NOTE: there should only be one gerbil registered so...
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
await deletePeer(exitNode.exitNodeId, publicKey);
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-proxy-mapping
|
|
||||||
client.registerHandler("remoteExitNode/update-proxy-mapping", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for proxy mapping update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-proxy-mapping`, message.data);
|
|
||||||
logger.info(`Successfully updated proxy mapping: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating proxy mapping:", {
|
|
||||||
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 updating proxy mapping:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// /update-destinations
|
|
||||||
client.registerHandler("remoteExitNode/update-destinations", async (message) => {
|
|
||||||
try {
|
|
||||||
const [exitNode] = await db.select().from(exitNodes).limit(1);
|
|
||||||
if (!exitNode) {
|
|
||||||
logger.error("No exit node found for destinations update");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await axios.post(`${exitNode.endpoint}/update-destinations`, message.data);
|
|
||||||
logger.info(`Successfully updated destinations: ${response.status}`);
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating destinations:", {
|
|
||||||
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 updating destinations:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
client.registerHandler("remoteExitNode/traefik/reload", async (message) => {
|
|
||||||
await monitor.HandleTraefikConfig();
|
|
||||||
});
|
|
||||||
|
|
||||||
// Listen to connection events
|
|
||||||
client.on("connect", () => {
|
|
||||||
logger.info("Connected to WebSocket server");
|
|
||||||
client.sendMessage("remoteExitNode/register", {
|
|
||||||
remoteExitNodeVersion: APP_VERSION
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("disconnect", () => {
|
|
||||||
logger.info("Disconnected from WebSocket server");
|
|
||||||
});
|
|
||||||
|
|
||||||
client.on("message", (message) => {
|
|
||||||
logger.info(
|
|
||||||
`Received message: ${message.type} ${JSON.stringify(message.data)}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Connect to the server
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
logger.info("Connection initiated");
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to connect:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store the ping interval stop function for cleanup if needed
|
|
||||||
const stopPingInterval = client.sendMessageInterval(
|
|
||||||
"remoteExitNode/ping",
|
|
||||||
{ timestamp: Date.now() / 1000 },
|
|
||||||
60000
|
|
||||||
); // send every minute
|
|
||||||
|
|
||||||
// Return client and cleanup function for potential use
|
|
||||||
return { client, stopPingInterval };
|
|
||||||
}
|
|
||||||
@@ -5,9 +5,15 @@ import { runSetupFunctions } from "./setup";
|
|||||||
import { createApiServer } from "./apiServer";
|
import { createApiServer } from "./apiServer";
|
||||||
import { createNextServer } from "./nextServer";
|
import { createNextServer } from "./nextServer";
|
||||||
import { createInternalServer } from "./internalServer";
|
import { createInternalServer } from "./internalServer";
|
||||||
import { ApiKey, ApiKeyOrg, RemoteExitNode, Session, User, UserOrg } from "@server/db";
|
import {
|
||||||
|
ApiKey,
|
||||||
|
ApiKeyOrg,
|
||||||
|
RemoteExitNode,
|
||||||
|
Session,
|
||||||
|
User,
|
||||||
|
UserOrg
|
||||||
|
} from "@server/db";
|
||||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||||
import { createHybridClientServer } from "./hybridServer";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { setHostMeta } from "@server/lib/hostMeta";
|
import { setHostMeta } from "@server/lib/hostMeta";
|
||||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||||
@@ -26,16 +32,11 @@ async function startServers() {
|
|||||||
const apiServer = createApiServer();
|
const apiServer = createApiServer();
|
||||||
const internalServer = createInternalServer();
|
const internalServer = createInternalServer();
|
||||||
|
|
||||||
let hybridClientServer;
|
|
||||||
let nextServer;
|
let nextServer;
|
||||||
if (config.isManagedMode()) {
|
nextServer = await createNextServer();
|
||||||
hybridClientServer = await createHybridClientServer();
|
if (config.getRawConfig().traefik.file_mode) {
|
||||||
} else {
|
const monitor = new TraefikConfigManager();
|
||||||
nextServer = await createNextServer();
|
await monitor.start();
|
||||||
if (config.getRawConfig().traefik.file_mode) {
|
|
||||||
const monitor = new TraefikConfigManager();
|
|
||||||
await monitor.start();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let integrationServer;
|
let integrationServer;
|
||||||
@@ -49,8 +50,7 @@ async function startServers() {
|
|||||||
apiServer,
|
apiServer,
|
||||||
nextServer,
|
nextServer,
|
||||||
internalServer,
|
internalServer,
|
||||||
integrationServer,
|
integrationServer
|
||||||
hybridClientServer
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,17 @@ interface StripeEvent {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function noop() {
|
||||||
|
if (
|
||||||
|
build !== "saas" ||
|
||||||
|
!process.env.S3_BUCKET ||
|
||||||
|
!process.env.LOCAL_FILE_PATH
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export class UsageService {
|
export class UsageService {
|
||||||
private cache: NodeCache;
|
private cache: NodeCache;
|
||||||
private bucketName: string | undefined;
|
private bucketName: string | undefined;
|
||||||
@@ -41,7 +52,7 @@ export class UsageService {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
this.cache = new NodeCache({ stdTTL: 300 }); // 5 minute TTL
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
// this.bucketName = privateConfig.getRawPrivateConfig().stripe?.s3Bucket;
|
||||||
@@ -71,7 +82,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async initializeEventsDirectory(): Promise<void> {
|
private async initializeEventsDirectory(): Promise<void> {
|
||||||
if (!this.eventsDir) {
|
if (!this.eventsDir) {
|
||||||
logger.warn("Stripe local file path is not configured, skipping events directory initialization.");
|
logger.warn(
|
||||||
|
"Stripe local file path is not configured, skipping events directory initialization."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -83,7 +96,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
private async uploadPendingEventFilesOnStartup(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping leftover event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping leftover event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +127,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -124,7 +141,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty startup file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty startup file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -135,8 +154,8 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
logger.error("Failed to scan for leftover event files:", err);
|
logger.error("Failed to scan for leftover event files");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,7 +165,7 @@ export class UsageService {
|
|||||||
value: number,
|
value: number,
|
||||||
transaction: any = null
|
transaction: any = null
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,7 +198,12 @@ export class UsageService {
|
|||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
usage = await this.internalAddUsage(orgId, featureId, value, trx);
|
usage = await this.internalAddUsage(
|
||||||
|
orgId,
|
||||||
|
featureId,
|
||||||
|
value,
|
||||||
|
trx
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,9 +213,10 @@ export class UsageService {
|
|||||||
return usage || null;
|
return usage || null;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if this is a deadlock error
|
// Check if this is a deadlock error
|
||||||
const isDeadlock = error?.code === '40P01' ||
|
const isDeadlock =
|
||||||
error?.cause?.code === '40P01' ||
|
error?.code === "40P01" ||
|
||||||
(error?.message && error.message.includes('deadlock'));
|
error?.cause?.code === "40P01" ||
|
||||||
|
(error?.message && error.message.includes("deadlock"));
|
||||||
|
|
||||||
if (isDeadlock && attempt < maxRetries) {
|
if (isDeadlock && attempt < maxRetries) {
|
||||||
attempt++;
|
attempt++;
|
||||||
@@ -204,7 +229,7 @@ export class UsageService {
|
|||||||
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
`Deadlock detected for ${orgId}/${featureId}, retrying attempt ${attempt}/${maxRetries} after ${delay.toFixed(0)}ms`
|
||||||
);
|
);
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -247,7 +272,8 @@ export class UsageService {
|
|||||||
set: {
|
set: {
|
||||||
latestValue: sql`${usage.latestValue} + ${value}`
|
latestValue: sql`${usage.latestValue} + ${value}`
|
||||||
}
|
}
|
||||||
}).returning();
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
return returnUsage;
|
return returnUsage;
|
||||||
}
|
}
|
||||||
@@ -268,7 +294,7 @@ export class UsageService {
|
|||||||
value?: number,
|
value?: number,
|
||||||
customerId?: string
|
customerId?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -432,7 +458,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
private async writeEventToFile(event: StripeEvent): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping event file write.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping event file write."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -481,7 +509,9 @@ export class UsageService {
|
|||||||
|
|
||||||
private async uploadFileToS3(): Promise<void> {
|
private async uploadFileToS3(): Promise<void> {
|
||||||
if (!this.bucketName || !this.eventsDir) {
|
if (!this.bucketName || !this.eventsDir) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping S3 upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping S3 upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!this.currentEventFile) {
|
if (!this.currentEventFile) {
|
||||||
@@ -493,7 +523,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Check if this file is already being uploaded
|
// Check if this file is already being uploaded
|
||||||
if (this.uploadingFiles.has(fileName)) {
|
if (this.uploadingFiles.has(fileName)) {
|
||||||
logger.debug(`File ${fileName} is already being uploaded, skipping`);
|
logger.debug(
|
||||||
|
`File ${fileName} is already being uploaded, skipping`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,7 +537,9 @@ export class UsageService {
|
|||||||
try {
|
try {
|
||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(`File ${fileName} does not exist, may have been already processed`);
|
logger.debug(
|
||||||
|
`File ${fileName} does not exist, may have been already processed`
|
||||||
|
);
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
// Reset current file if it was this file
|
// Reset current file if it was this file
|
||||||
if (this.currentEventFile === fileName) {
|
if (this.currentEventFile === fileName) {
|
||||||
@@ -525,7 +559,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted
|
// File may have been already deleted
|
||||||
logger.debug(`File ${fileName} was already deleted during cleanup`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during cleanup`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -548,7 +584,9 @@ export class UsageService {
|
|||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
// File may have been already deleted by another process
|
// File may have been already deleted by another process
|
||||||
logger.debug(`File ${fileName} was already deleted during upload`);
|
logger.debug(
|
||||||
|
`File ${fileName} was already deleted during upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -559,10 +597,7 @@ export class UsageService {
|
|||||||
this.currentEventFile = null;
|
this.currentEventFile = null;
|
||||||
this.currentFileStartTime = 0;
|
this.currentFileStartTime = 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(`Failed to upload ${fileName} to S3:`, error);
|
||||||
`Failed to upload ${fileName} to S3:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
} finally {
|
} finally {
|
||||||
// Always remove from uploading set
|
// Always remove from uploading set
|
||||||
this.uploadingFiles.delete(fileName);
|
this.uploadingFiles.delete(fileName);
|
||||||
@@ -579,7 +614,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -653,7 +688,7 @@ export class UsageService {
|
|||||||
orgId: string,
|
orgId: string,
|
||||||
featureId: FeatureId
|
featureId: FeatureId
|
||||||
): Promise<Usage | null> {
|
): Promise<Usage | null> {
|
||||||
if (build !== "saas") {
|
if (noop()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
await this.updateDaily(orgId, featureId); // Ensure daily usage is updated
|
||||||
@@ -673,7 +708,9 @@ export class UsageService {
|
|||||||
*/
|
*/
|
||||||
private async uploadOldEventFiles(): Promise<void> {
|
private async uploadOldEventFiles(): Promise<void> {
|
||||||
if (!this.eventsDir || !this.bucketName) {
|
if (!this.eventsDir || !this.bucketName) {
|
||||||
logger.warn("Stripe local file path or bucket name is not configured, skipping old event file upload.");
|
logger.warn(
|
||||||
|
"Stripe local file path or bucket name is not configured, skipping old event file upload."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -684,7 +721,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// Skip files that are already being uploaded
|
// Skip files that are already being uploaded
|
||||||
if (this.uploadingFiles.has(file)) {
|
if (this.uploadingFiles.has(file)) {
|
||||||
logger.debug(`Skipping file ${file} as it's already being uploaded`);
|
logger.debug(
|
||||||
|
`Skipping file ${file} as it's already being uploaded`
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -726,7 +765,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`File ${file} was already deleted during interval upload`);
|
logger.debug(
|
||||||
|
`File ${file} was already deleted during interval upload`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -743,7 +784,9 @@ export class UsageService {
|
|||||||
await fs.access(filePath);
|
await fs.access(filePath);
|
||||||
await fs.unlink(filePath);
|
await fs.unlink(filePath);
|
||||||
} catch (unlinkError) {
|
} catch (unlinkError) {
|
||||||
logger.debug(`Empty file ${file} was already deleted`);
|
logger.debug(
|
||||||
|
`Empty file ${file} was already deleted`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
@@ -765,8 +808,13 @@ export class UsageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkLimitSet(orgId: string, kickSites = false, featureId?: FeatureId, usage?: Usage): Promise<boolean> {
|
public async checkLimitSet(
|
||||||
if (build !== "saas") {
|
orgId: string,
|
||||||
|
kickSites = false,
|
||||||
|
featureId?: FeatureId,
|
||||||
|
usage?: Usage
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (noop()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// This method should check the current usage against the limits set for the organization
|
// This method should check the current usage against the limits set for the organization
|
||||||
@@ -805,16 +853,30 @@ export class UsageService {
|
|||||||
if (usage) {
|
if (usage) {
|
||||||
currentUsage = usage;
|
currentUsage = usage;
|
||||||
} else {
|
} else {
|
||||||
currentUsage = await this.getUsage(orgId, limit.featureId as FeatureId);
|
currentUsage = await this.getUsage(
|
||||||
|
orgId,
|
||||||
|
limit.featureId as FeatureId
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const usageValue = currentUsage?.instantaneousValue || currentUsage?.latestValue || 0;
|
const usageValue =
|
||||||
logger.debug(`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`);
|
currentUsage?.instantaneousValue ||
|
||||||
logger.debug(`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`);
|
currentUsage?.latestValue ||
|
||||||
if (currentUsage && limit.value !== null && usageValue > limit.value) {
|
0;
|
||||||
|
logger.debug(
|
||||||
|
`Current usage for org ${orgId} on feature ${limit.featureId}: ${usageValue}`
|
||||||
|
);
|
||||||
|
logger.debug(
|
||||||
|
`Limit for org ${orgId} on feature ${limit.featureId}: ${limit.value}`
|
||||||
|
);
|
||||||
|
if (
|
||||||
|
currentUsage &&
|
||||||
|
limit.value !== null &&
|
||||||
|
usageValue > limit.value
|
||||||
|
) {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
`Org ${orgId} has exceeded limit for ${limit.featureId}: ` +
|
||||||
`${usageValue} > ${limit.value}`
|
`${usageValue} > ${limit.value}`
|
||||||
);
|
);
|
||||||
hasExceededLimits = true;
|
hasExceededLimits = true;
|
||||||
break; // Exit early if any limit is exceeded
|
break; // Exit early if any limit is exceeded
|
||||||
@@ -823,7 +885,9 @@ export class UsageService {
|
|||||||
|
|
||||||
// If any limits are exceeded, disconnect all sites for this organization
|
// If any limits are exceeded, disconnect all sites for this organization
|
||||||
if (hasExceededLimits && kickSites) {
|
if (hasExceededLimits && kickSites) {
|
||||||
logger.warn(`Disconnecting all sites for org ${orgId} due to exceeded limits`);
|
logger.warn(
|
||||||
|
`Disconnecting all sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
|
|
||||||
// Get all sites for this organization
|
// Get all sites for this organization
|
||||||
const orgSites = await db
|
const orgSites = await db
|
||||||
@@ -832,7 +896,7 @@ export class UsageService {
|
|||||||
.where(eq(sites.orgId, orgId));
|
.where(eq(sites.orgId, orgId));
|
||||||
|
|
||||||
// Mark all sites as offline and send termination messages
|
// Mark all sites as offline and send termination messages
|
||||||
const siteUpdates = orgSites.map(site => site.siteId);
|
const siteUpdates = orgSites.map((site) => site.siteId);
|
||||||
|
|
||||||
if (siteUpdates.length > 0) {
|
if (siteUpdates.length > 0) {
|
||||||
// Send termination messages to newt sites
|
// Send termination messages to newt sites
|
||||||
@@ -853,17 +917,21 @@ export class UsageService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Don't await to prevent blocking
|
// Don't await to prevent blocking
|
||||||
sendToClient(newt.newtId, payload).catch((error: any) => {
|
sendToClient(newt.newtId, payload).catch(
|
||||||
logger.error(
|
(error: any) => {
|
||||||
`Failed to send termination message to newt ${newt.newtId}:`,
|
logger.error(
|
||||||
error
|
`Failed to send termination message to newt ${newt.newtId}:`,
|
||||||
);
|
error
|
||||||
});
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`);
|
logger.info(
|
||||||
|
`Disconnected ${orgSites.length} sites for org ${orgId} due to exceeded limits`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -1,70 +1,3 @@
|
|||||||
import axios from "axios";
|
|
||||||
import { tokenManager } from "./tokenManager";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import config from "./config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get valid certificates for the specified domains
|
|
||||||
*/
|
|
||||||
export async function getValidCertificatesForDomainsHybrid(domains: Set<string>): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
if (domains.size === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const domainArray = Array.from(domains);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/certificates/domains`,
|
|
||||||
{
|
|
||||||
params: {
|
|
||||||
domains: domainArray
|
|
||||||
},
|
|
||||||
headers: (await tokenManager.getAuthHeader()).headers
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch certificates for domains: ${response.status} ${response.statusText}`,
|
|
||||||
{ responseData: response.data, domains: domainArray }
|
|
||||||
);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// logger.debug(
|
|
||||||
// `Successfully retrieved ${response.data.data?.length || 0} certificates for ${domainArray.length} domains`
|
|
||||||
// );
|
|
||||||
|
|
||||||
return response.data.data;
|
|
||||||
} catch (error) {
|
|
||||||
// pull data out of the axios error to log
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error getting certificates:", {
|
|
||||||
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 getting certificates:", error);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
export async function getValidCertificatesForDomains(domains: Set<string>): Promise<
|
||||||
Array<{
|
Array<{
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -102,10 +102,7 @@ export class Config {
|
|||||||
if (!this.rawConfig) {
|
if (!this.rawConfig) {
|
||||||
throw new Error("Config not loaded. Call load() first.");
|
throw new Error("Config not loaded. Call load() first.");
|
||||||
}
|
}
|
||||||
if (this.rawConfig.managed) {
|
|
||||||
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN MANAGED
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
license.setServerSecret(this.rawConfig.server.secret!);
|
license.setServerSecret(this.rawConfig.server.secret!);
|
||||||
|
|
||||||
await this.checkKeyStatus();
|
await this.checkKeyStatus();
|
||||||
@@ -158,10 +155,6 @@ export class Config {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isManagedMode() {
|
|
||||||
return typeof this.rawConfig?.managed === "object";
|
|
||||||
}
|
|
||||||
|
|
||||||
public async checkSupporterKey() {
|
public async checkSupporterKey() {
|
||||||
const [key] = await db.select().from(supporterKey).limit(1);
|
const [key] = await db.select().from(supporterKey).limit(1);
|
||||||
|
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { defaultRoleAllowedActions } from "@server/routers/role";
|
import { defaultRoleAllowedActions } from "@server/routers/role";
|
||||||
import { FeatureId, limitsService, sandboxLimitSet } from "@server/lib/billing";
|
import { FeatureId, limitsService, sandboxLimitSet, createCustomer } from "@server/lib/billing";
|
||||||
import { createCustomer } from "@server/private/lib/billing/createCustomer";
|
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
|
|
||||||
export async function createUserAccountOrg(
|
export async function createUserAccountOrg(
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
import axios from "axios";
|
|
||||||
import config from "./config";
|
|
||||||
import { tokenManager } from "./tokenManager";
|
|
||||||
|
|
||||||
export async function getCountryCodeForIp(
|
export async function getCountryCodeForIp(
|
||||||
ip: string
|
ip: string
|
||||||
@@ -34,31 +31,3 @@ export async function getCountryCodeForIp(
|
|||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function remoteGetCountryCodeForIp(
|
|
||||||
ip: string
|
|
||||||
): Promise<string | undefined> {
|
|
||||||
try {
|
|
||||||
const response = await axios.get(
|
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/geoip/${ip}`,
|
|
||||||
await tokenManager.getAuthHeader()
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data.data.countryCode;
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error fetching config in verify session:", {
|
|
||||||
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 fetching config in verify session:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -42,18 +42,6 @@ export const configSchema = z
|
|||||||
anonymous_usage: true
|
anonymous_usage: true
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
managed: z
|
|
||||||
.object({
|
|
||||||
name: z.string().optional(),
|
|
||||||
id: z.string().optional(),
|
|
||||||
secret: z.string().optional(),
|
|
||||||
endpoint: z
|
|
||||||
.string()
|
|
||||||
.optional()
|
|
||||||
.default("https://pangolin.fossorial.io"),
|
|
||||||
redirect_endpoint: z.string().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
domains: z
|
domains: z
|
||||||
.record(
|
.record(
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -346,10 +334,7 @@ export const configSchema = z
|
|||||||
if (data.flags?.disable_config_managed_domains) {
|
if (data.flags?.disable_config_managed_domains) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// If hybrid is defined, domains are not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (keys.length === 0) {
|
if (keys.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -361,10 +346,6 @@ export const configSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// If hybrid is defined, server secret is not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
|
// If hybrid is not defined, server secret must be defined. If its not defined already then pull it from env
|
||||||
if (data.server?.secret === undefined) {
|
if (data.server?.secret === undefined) {
|
||||||
data.server.secret = process.env.SERVER_SECRET;
|
data.server.secret = process.env.SERVER_SECRET;
|
||||||
@@ -380,10 +361,6 @@ export const configSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
// If hybrid is defined, dashboard_url is not required
|
|
||||||
if (data.managed) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// If hybrid is not defined, dashboard_url must be defined
|
// If hybrid is not defined, dashboard_url must be defined
|
||||||
return (
|
return (
|
||||||
data.app.dashboard_url !== undefined &&
|
data.app.dashboard_url !== undefined &&
|
||||||
|
|||||||
@@ -1,73 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
|
||||||
import { Router } from "express";
|
|
||||||
import axios from "axios";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import createHttpError from "http-errors";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import { tokenManager } from "./tokenManager";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Proxy function that forwards requests to the remote cloud server
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const proxyToRemote = async (
|
|
||||||
req: Request,
|
|
||||||
res: Response,
|
|
||||||
next: NextFunction,
|
|
||||||
endpoint: string
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
|
||||||
const remoteUrl = `${config.getRawConfig().managed?.endpoint?.replace(/\/$/, '')}/api/v1/${endpoint}`;
|
|
||||||
|
|
||||||
logger.debug(`Proxying request to remote server: ${remoteUrl}`);
|
|
||||||
|
|
||||||
// Forward the request to the remote server
|
|
||||||
const response = await axios({
|
|
||||||
method: req.method as any,
|
|
||||||
url: remoteUrl,
|
|
||||||
data: req.body,
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...(await tokenManager.getAuthHeader()).headers
|
|
||||||
},
|
|
||||||
params: req.query,
|
|
||||||
timeout: 30000, // 30 second timeout
|
|
||||||
validateStatus: () => true // Don't throw on non-2xx status codes
|
|
||||||
});
|
|
||||||
|
|
||||||
logger.debug(`Proxy response: ${JSON.stringify(response.data)}`);
|
|
||||||
|
|
||||||
// Forward the response status and data
|
|
||||||
return res.status(response.status).json(response.data);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Error proxying request to remote server:", error);
|
|
||||||
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
if (error.code === 'ECONNREFUSED' || error.code === 'ENOTFOUND') {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.SERVICE_UNAVAILABLE,
|
|
||||||
"Remote server is unavailable"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (error.code === 'ECONNABORTED') {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.REQUEST_TIMEOUT,
|
|
||||||
"Request to remote server timed out"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.INTERNAL_SERVER_ERROR,
|
|
||||||
"Error communicating with remote server"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,274 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import logger from "@server/logger";
|
|
||||||
|
|
||||||
export interface TokenResponse {
|
|
||||||
success: boolean;
|
|
||||||
message?: string;
|
|
||||||
data: {
|
|
||||||
token: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token Manager - Handles automatic token refresh for hybrid server authentication
|
|
||||||
*
|
|
||||||
* Usage throughout the application:
|
|
||||||
* ```typescript
|
|
||||||
* import { tokenManager } from "@server/lib/tokenManager";
|
|
||||||
*
|
|
||||||
* // Get the current valid token
|
|
||||||
* const token = await tokenManager.getToken();
|
|
||||||
*
|
|
||||||
* // Force refresh if needed
|
|
||||||
* await tokenManager.refreshToken();
|
|
||||||
* ```
|
|
||||||
*
|
|
||||||
* The token manager automatically refreshes tokens every 24 hours by default
|
|
||||||
* and is started once in the privateHybridServer.ts file.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class TokenManager {
|
|
||||||
private token: string | null = null;
|
|
||||||
private refreshInterval: NodeJS.Timeout | null = null;
|
|
||||||
private isRefreshing: boolean = false;
|
|
||||||
private refreshIntervalMs: number;
|
|
||||||
private retryInterval: NodeJS.Timeout | null = null;
|
|
||||||
private retryIntervalMs: number;
|
|
||||||
private tokenAvailablePromise: Promise<void> | null = null;
|
|
||||||
private tokenAvailableResolve: (() => void) | null = null;
|
|
||||||
|
|
||||||
constructor(refreshIntervalMs: number = 24 * 60 * 60 * 1000, retryIntervalMs: number = 5000) {
|
|
||||||
// Default to 24 hours for refresh, 5 seconds for retry
|
|
||||||
this.refreshIntervalMs = refreshIntervalMs;
|
|
||||||
this.retryIntervalMs = retryIntervalMs;
|
|
||||||
this.setupTokenAvailablePromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up promise that resolves when token becomes available
|
|
||||||
*/
|
|
||||||
private setupTokenAvailablePromise(): void {
|
|
||||||
this.tokenAvailablePromise = new Promise((resolve) => {
|
|
||||||
this.tokenAvailableResolve = resolve;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve the token available promise
|
|
||||||
*/
|
|
||||||
private resolveTokenAvailable(): void {
|
|
||||||
if (this.tokenAvailableResolve) {
|
|
||||||
this.tokenAvailableResolve();
|
|
||||||
this.tokenAvailableResolve = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the token manager - gets initial token and sets up refresh interval
|
|
||||||
* If initial token fetch fails, keeps retrying every few seconds until successful
|
|
||||||
*/
|
|
||||||
async start(): Promise<void> {
|
|
||||||
logger.info("Starting token manager...");
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.refreshToken();
|
|
||||||
this.setupRefreshInterval();
|
|
||||||
this.resolveTokenAvailable();
|
|
||||||
logger.info("Token manager started successfully");
|
|
||||||
} catch (error) {
|
|
||||||
logger.warn(`Failed to get initial token, will retry in ${this.retryIntervalMs / 1000} seconds:`, error);
|
|
||||||
this.setupRetryInterval();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up retry interval for initial token acquisition
|
|
||||||
*/
|
|
||||||
private setupRetryInterval(): void {
|
|
||||||
if (this.retryInterval) {
|
|
||||||
clearInterval(this.retryInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.retryInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
logger.debug("Retrying initial token acquisition");
|
|
||||||
await this.refreshToken();
|
|
||||||
this.setupRefreshInterval();
|
|
||||||
this.clearRetryInterval();
|
|
||||||
this.resolveTokenAvailable();
|
|
||||||
logger.info("Token manager started successfully after retry");
|
|
||||||
} catch (error) {
|
|
||||||
logger.debug("Token acquisition retry failed, will try again");
|
|
||||||
}
|
|
||||||
}, this.retryIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear retry interval
|
|
||||||
*/
|
|
||||||
private clearRetryInterval(): void {
|
|
||||||
if (this.retryInterval) {
|
|
||||||
clearInterval(this.retryInterval);
|
|
||||||
this.retryInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Stop the token manager and clear all intervals
|
|
||||||
*/
|
|
||||||
stop(): void {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
this.refreshInterval = null;
|
|
||||||
}
|
|
||||||
this.clearRetryInterval();
|
|
||||||
logger.info("Token manager stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current valid token
|
|
||||||
*/
|
|
||||||
|
|
||||||
// TODO: WE SHOULD NOT BE GETTING A TOKEN EVERY TIME WE REQUEST IT
|
|
||||||
async getToken(): Promise<string> {
|
|
||||||
// If we don't have a token yet, wait for it to become available
|
|
||||||
if (!this.token && this.tokenAvailablePromise) {
|
|
||||||
await this.tokenAvailablePromise;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.token) {
|
|
||||||
if (this.isRefreshing) {
|
|
||||||
// Wait for current refresh to complete
|
|
||||||
await this.waitForRefresh();
|
|
||||||
} else {
|
|
||||||
throw new Error("No valid token available");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this.token) {
|
|
||||||
throw new Error("No valid token available");
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.token;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getAuthHeader() {
|
|
||||||
return {
|
|
||||||
headers: {
|
|
||||||
Authorization: `Bearer ${await this.getToken()}`,
|
|
||||||
"X-CSRF-Token": "x-csrf-protection",
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Force refresh the token
|
|
||||||
*/
|
|
||||||
async refreshToken(): Promise<void> {
|
|
||||||
if (this.isRefreshing) {
|
|
||||||
await this.waitForRefresh();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isRefreshing = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const hybridConfig = config.getRawConfig().managed;
|
|
||||||
|
|
||||||
if (
|
|
||||||
!hybridConfig?.id ||
|
|
||||||
!hybridConfig?.secret ||
|
|
||||||
!hybridConfig?.endpoint
|
|
||||||
) {
|
|
||||||
throw new Error("Hybrid configuration is not defined");
|
|
||||||
}
|
|
||||||
|
|
||||||
const tokenEndpoint = `${hybridConfig.endpoint}/api/v1/auth/remoteExitNode/get-token`;
|
|
||||||
|
|
||||||
const tokenData = {
|
|
||||||
remoteExitNodeId: hybridConfig.id,
|
|
||||||
secret: hybridConfig.secret
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Requesting new token from server");
|
|
||||||
|
|
||||||
const response = await axios.post<TokenResponse>(
|
|
||||||
tokenEndpoint,
|
|
||||||
tokenData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
"X-CSRF-Token": "x-csrf-protection"
|
|
||||||
},
|
|
||||||
timeout: 10000 // 10 second timeout
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.data.success) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to get token: ${response.data.message}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.data.data.token) {
|
|
||||||
throw new Error("Received empty token from server");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.token = response.data.data.token;
|
|
||||||
logger.debug("Token refreshed successfully");
|
|
||||||
} catch (error) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
logger.error("Error updating proxy mapping:", {
|
|
||||||
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 updating proxy mapping:", error);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Failed to refresh token");
|
|
||||||
} finally {
|
|
||||||
this.isRefreshing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set up automatic token refresh interval
|
|
||||||
*/
|
|
||||||
private setupRefreshInterval(): void {
|
|
||||||
if (this.refreshInterval) {
|
|
||||||
clearInterval(this.refreshInterval);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refreshInterval = setInterval(async () => {
|
|
||||||
try {
|
|
||||||
logger.debug("Auto-refreshing token");
|
|
||||||
await this.refreshToken();
|
|
||||||
} catch (error) {
|
|
||||||
logger.error("Failed to auto-refresh token:", error);
|
|
||||||
}
|
|
||||||
}, this.refreshIntervalMs);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wait for current refresh operation to complete
|
|
||||||
*/
|
|
||||||
private async waitForRefresh(): Promise<void> {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const checkInterval = setInterval(() => {
|
|
||||||
if (!this.isRefreshing) {
|
|
||||||
clearInterval(checkInterval);
|
|
||||||
resolve();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Export a singleton instance for use throughout the application
|
|
||||||
export const tokenManager = new TokenManager();
|
|
||||||
@@ -6,13 +6,9 @@ import * as yaml from "js-yaml";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { db, exitNodes } from "@server/db";
|
import { db, exitNodes } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { tokenManager } from "../tokenManager";
|
|
||||||
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
import { getCurrentExitNodeId } from "@server/lib/exitNodes";
|
||||||
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
import { getTraefikConfig } from "#dynamic/lib/traefik";
|
||||||
import {
|
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
|
||||||
getValidCertificatesForDomains,
|
|
||||||
getValidCertificatesForDomainsHybrid
|
|
||||||
} from "#dynamic/lib/certificates";
|
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
@@ -313,93 +309,92 @@ export class TraefikConfigManager {
|
|||||||
this.lastActiveDomains = new Set(domains);
|
this.lastActiveDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan current local certificate state
|
if (
|
||||||
this.lastLocalCertificateState =
|
process.env.GENERATE_OWN_CERTIFICATES === "true" &&
|
||||||
await this.scanLocalCertificateState();
|
build != "oss"
|
||||||
|
) {
|
||||||
|
// Scan current local certificate state
|
||||||
|
this.lastLocalCertificateState =
|
||||||
|
await this.scanLocalCertificateState();
|
||||||
|
|
||||||
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
// Only fetch certificates if needed (domain changes, missing certs, or daily renewal check)
|
||||||
let validCertificates: Array<{
|
let validCertificates: Array<{
|
||||||
id: number;
|
id: number;
|
||||||
domain: string;
|
domain: string;
|
||||||
wildcard: boolean | null;
|
wildcard: boolean | null;
|
||||||
certFile: string | null;
|
certFile: string | null;
|
||||||
keyFile: string | null;
|
keyFile: string | null;
|
||||||
expiresAt: number | null;
|
expiresAt: number | null;
|
||||||
updatedAt?: number | null;
|
updatedAt?: number | null;
|
||||||
}> = [];
|
}> = [];
|
||||||
|
|
||||||
if (this.shouldFetchCertificates(domains)) {
|
if (this.shouldFetchCertificates(domains)) {
|
||||||
// Filter out domains that are already covered by wildcard certificates
|
// Filter out domains that are already covered by wildcard certificates
|
||||||
const domainsToFetch = new Set<string>();
|
const domainsToFetch = new Set<string>();
|
||||||
for (const domain of domains) {
|
for (const domain of domains) {
|
||||||
if (
|
if (
|
||||||
!isDomainCoveredByWildcard(
|
!isDomainCoveredByWildcard(
|
||||||
domain,
|
domain,
|
||||||
this.lastLocalCertificateState
|
this.lastLocalCertificateState
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
domainsToFetch.add(domain);
|
domainsToFetch.add(domain);
|
||||||
} else {
|
} else {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
`Domain ${domain} is covered by existing wildcard certificate, skipping fetch`
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (domainsToFetch.size > 0) {
|
|
||||||
// Get valid certificates for domains not covered by wildcards
|
|
||||||
if (config.isManagedMode()) {
|
|
||||||
validCertificates =
|
|
||||||
await getValidCertificatesForDomainsHybrid(
|
|
||||||
domainsToFetch
|
|
||||||
);
|
);
|
||||||
} else {
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (domainsToFetch.size > 0) {
|
||||||
|
// Get valid certificates for domains not covered by wildcards
|
||||||
validCertificates =
|
validCertificates =
|
||||||
await getValidCertificatesForDomains(
|
await getValidCertificatesForDomains(
|
||||||
domainsToFetch
|
domainsToFetch
|
||||||
);
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Download and decrypt new certificates
|
||||||
|
await this.processValidCertificates(validCertificates);
|
||||||
|
} else {
|
||||||
|
logger.info(
|
||||||
|
"All domains are covered by existing wildcard certificates, no fetch needed"
|
||||||
|
);
|
||||||
|
this.lastCertificateFetch = new Date();
|
||||||
|
this.lastKnownDomains = new Set(domains);
|
||||||
}
|
}
|
||||||
this.lastCertificateFetch = new Date();
|
|
||||||
this.lastKnownDomains = new Set(domains);
|
|
||||||
|
|
||||||
logger.info(
|
// Always ensure all existing certificates (including wildcards) are in the config
|
||||||
`Fetched ${validCertificates.length} certificates from remote (${domains.size - domainsToFetch.size} domains covered by wildcards)`
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
);
|
|
||||||
|
|
||||||
// Download and decrypt new certificates
|
|
||||||
await this.processValidCertificates(validCertificates);
|
|
||||||
} else {
|
} else {
|
||||||
logger.info(
|
const timeSinceLastFetch = this.lastCertificateFetch
|
||||||
"All domains are covered by existing wildcard certificates, no fetch needed"
|
? Math.round(
|
||||||
);
|
(Date.now() -
|
||||||
this.lastCertificateFetch = new Date();
|
this.lastCertificateFetch.getTime()) /
|
||||||
this.lastKnownDomains = new Set(domains);
|
(1000 * 60)
|
||||||
|
)
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
// logger.debug(
|
||||||
|
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
||||||
|
// );
|
||||||
|
|
||||||
|
// Still need to ensure config is up to date with existing certificates
|
||||||
|
await this.updateDynamicConfigFromLocalCerts(domains);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure all existing certificates (including wildcards) are in the config
|
// Clean up certificates for domains no longer in use
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
await this.cleanupUnusedCertificates(domains);
|
||||||
} else {
|
|
||||||
const timeSinceLastFetch = this.lastCertificateFetch
|
|
||||||
? Math.round(
|
|
||||||
(Date.now() - this.lastCertificateFetch.getTime()) /
|
|
||||||
(1000 * 60)
|
|
||||||
)
|
|
||||||
: 0;
|
|
||||||
|
|
||||||
// logger.debug(
|
// wait 1 second for traefik to pick up the new certificates
|
||||||
// `Skipping certificate fetch - no changes detected and within 24-hour window (last fetch: ${timeSinceLastFetch} minutes ago)`
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
// );
|
|
||||||
|
|
||||||
// Still need to ensure config is up to date with existing certificates
|
|
||||||
await this.updateDynamicConfigFromLocalCerts(domains);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clean up certificates for domains no longer in use
|
|
||||||
await this.cleanupUnusedCertificates(domains);
|
|
||||||
|
|
||||||
// wait 1 second for traefik to pick up the new certificates
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
||||||
|
|
||||||
// Write traefik config as YAML to a second dynamic config file if changed
|
// Write traefik config as YAML to a second dynamic config file if changed
|
||||||
await this.writeTraefikDynamicConfig(traefikConfig);
|
await this.writeTraefikDynamicConfig(traefikConfig);
|
||||||
|
|
||||||
@@ -448,32 +443,15 @@ export class TraefikConfigManager {
|
|||||||
} | null> {
|
} | null> {
|
||||||
let traefikConfig;
|
let traefikConfig;
|
||||||
try {
|
try {
|
||||||
if (config.isManagedMode()) {
|
const currentExitNode = await getCurrentExitNodeId();
|
||||||
const resp = await axios.get(
|
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
||||||
`${config.getRawConfig().managed?.endpoint}/api/v1/hybrid/traefik-config`,
|
traefikConfig = await getTraefikConfig(
|
||||||
await tokenManager.getAuthHeader()
|
// this is called by the local exit node to get its own config
|
||||||
);
|
currentExitNode,
|
||||||
|
config.getRawConfig().traefik.site_types,
|
||||||
if (resp.status !== 200) {
|
build == "oss", // filter out the namespace domains in open source
|
||||||
logger.error(
|
build != "oss" // generate the login pages on the cloud and hybrid
|
||||||
`Failed to fetch traefik config: ${resp.status} ${resp.statusText}`,
|
);
|
||||||
{ responseData: resp.data }
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
traefikConfig = resp.data.data;
|
|
||||||
} else {
|
|
||||||
const currentExitNode = await getCurrentExitNodeId();
|
|
||||||
// logger.debug(`Fetching traefik config for exit node: ${currentExitNode}`);
|
|
||||||
traefikConfig = await getTraefikConfig(
|
|
||||||
// this is called by the local exit node to get its own config
|
|
||||||
currentExitNode,
|
|
||||||
config.getRawConfig().traefik.site_types,
|
|
||||||
build == "oss", // filter out the namespace domains in open source
|
|
||||||
build != "oss" // generate the login pages on the cloud and hybrid
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const domains = new Set<string>();
|
const domains = new Set<string>();
|
||||||
|
|
||||||
@@ -718,7 +696,12 @@ export class TraefikConfigManager {
|
|||||||
|
|
||||||
for (const cert of validCertificates) {
|
for (const cert of validCertificates) {
|
||||||
try {
|
try {
|
||||||
if (!cert.certFile || !cert.keyFile) {
|
if (
|
||||||
|
!cert.certFile ||
|
||||||
|
!cert.keyFile ||
|
||||||
|
cert.certFile.length === 0 ||
|
||||||
|
cert.keyFile.length === 0
|
||||||
|
) {
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`Certificate for domain ${cert.domain} is missing cert or key file`
|
`Certificate for domain ${cert.domain} is missing cert or key file`
|
||||||
);
|
);
|
||||||
@@ -842,7 +825,9 @@ export class TraefikConfigManager {
|
|||||||
const lastUpdateStr = fs
|
const lastUpdateStr = fs
|
||||||
.readFileSync(lastUpdatePath, "utf8")
|
.readFileSync(lastUpdatePath, "utf8")
|
||||||
.trim();
|
.trim();
|
||||||
lastUpdateTime = Math.floor(new Date(lastUpdateStr).getTime() / 1000);
|
lastUpdateTime = Math.floor(
|
||||||
|
new Date(lastUpdateStr).getTime() / 1000
|
||||||
|
);
|
||||||
} catch {
|
} catch {
|
||||||
lastUpdateTime = null;
|
lastUpdateTime = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -105,7 +105,12 @@ export async function getTraefikConfig(
|
|||||||
const priority = row.priority ?? 100;
|
const priority = row.priority ?? 100;
|
||||||
|
|
||||||
// Create a unique key combining resourceId, path config, and rewrite config
|
// Create a unique key combining resourceId, path config, and rewrite config
|
||||||
const pathKey = [targetPath, pathMatchType, rewritePath, rewritePathType]
|
const pathKey = [
|
||||||
|
targetPath,
|
||||||
|
pathMatchType,
|
||||||
|
rewritePath,
|
||||||
|
rewritePathType
|
||||||
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("-");
|
.join("-");
|
||||||
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
const mapKey = [resourceId, pathKey].filter(Boolean).join("-");
|
||||||
@@ -120,7 +125,9 @@ export async function getTraefikConfig(
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!validation.isValid) {
|
if (!validation.isValid) {
|
||||||
logger.error(`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`);
|
logger.error(
|
||||||
|
`Invalid path rewrite configuration for resource ${resourceId}: ${validation.error}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,21 +246,18 @@ export async function getTraefikConfig(
|
|||||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
let tls = {};
|
const tls = {
|
||||||
if (build == "oss") {
|
certResolver: certResolver,
|
||||||
tls = {
|
...(preferWildcardCert
|
||||||
certResolver: certResolver,
|
? {
|
||||||
...(preferWildcardCert
|
domains: [
|
||||||
? {
|
{
|
||||||
domains: [
|
main: wildCard
|
||||||
{
|
}
|
||||||
main: wildCard
|
]
|
||||||
}
|
}
|
||||||
]
|
: {})
|
||||||
}
|
};
|
||||||
: {})
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
@@ -264,11 +268,12 @@ export async function getTraefikConfig(
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Handle path rewriting middleware
|
// Handle path rewriting middleware
|
||||||
if (resource.rewritePath &&
|
if (
|
||||||
|
resource.rewritePath &&
|
||||||
resource.path &&
|
resource.path &&
|
||||||
resource.pathMatchType &&
|
resource.pathMatchType &&
|
||||||
resource.rewritePathType) {
|
resource.rewritePathType
|
||||||
|
) {
|
||||||
// Create a unique middleware name
|
// Create a unique middleware name
|
||||||
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
const rewriteMiddlewareName = `rewrite-r${resource.resourceId}-${key}`;
|
||||||
|
|
||||||
@@ -287,7 +292,10 @@ export async function getTraefikConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// the middleware to the config
|
// the middleware to the config
|
||||||
Object.assign(config_output.http.middlewares, rewriteResult.middlewares);
|
Object.assign(
|
||||||
|
config_output.http.middlewares,
|
||||||
|
rewriteResult.middlewares
|
||||||
|
);
|
||||||
|
|
||||||
// middlewares to the router middleware chain
|
// middlewares to the router middleware chain
|
||||||
if (rewriteResult.chain) {
|
if (rewriteResult.chain) {
|
||||||
@@ -298,9 +306,13 @@ export async function getTraefikConfig(
|
|||||||
routerMiddlewares.push(rewriteMiddlewareName);
|
routerMiddlewares.push(rewriteMiddlewareName);
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug(`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`);
|
logger.debug(
|
||||||
|
`Created path rewrite middleware ${rewriteMiddlewareName}: ${resource.pathMatchType}(${resource.path}) -> ${resource.rewritePathType}(${resource.rewritePath})`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`);
|
logger.error(
|
||||||
|
`Failed to create path rewrite middleware for resource ${resource.resourceId}: ${error}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,7 +328,9 @@ export async function getTraefikConfig(
|
|||||||
value: string;
|
value: string;
|
||||||
}[];
|
}[];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.warn(`Failed to parse headers for resource ${resource.resourceId}: ${e}`);
|
logger.warn(
|
||||||
|
`Failed to parse headers for resource ${resource.resourceId}: ${e}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
headersArr.forEach((header) => {
|
headersArr.forEach((header) => {
|
||||||
@@ -482,14 +496,14 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
cookie: {
|
cookie: {
|
||||||
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
name: "p_sticky", // TODO: make this configurable via config.yml like other cookies
|
||||||
secure: resource.ssl,
|
secure: resource.ssl,
|
||||||
httpOnly: true
|
httpOnly: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -590,13 +604,13 @@ export async function getTraefikConfig(
|
|||||||
})(),
|
})(),
|
||||||
...(resource.stickySession
|
...(resource.stickySession
|
||||||
? {
|
? {
|
||||||
sticky: {
|
sticky: {
|
||||||
ipStrategy: {
|
ipStrategy: {
|
||||||
depth: 0,
|
depth: 0,
|
||||||
sourcePort: true
|
sourcePort: true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
: {})
|
: {})
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* This file is part of a proprietary work.
|
||||||
|
*
|
||||||
|
* Copyright (c) 2025 Fossorial, Inc.
|
||||||
|
* All rights reserved.
|
||||||
|
*
|
||||||
|
* This file is licensed under the Fossorial Commercial License.
|
||||||
|
* You may not use this file except in compliance with the License.
|
||||||
|
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||||
|
*
|
||||||
|
* This file is not licensed under the AGPLv3.
|
||||||
|
*/
|
||||||
|
|
||||||
import { rateLimitService } from "#private/lib/rateLimit";
|
import { rateLimitService } from "#private/lib/rateLimit";
|
||||||
import { cleanup as wsCleanup } from "#private/routers/ws";
|
import { cleanup as wsCleanup } from "#private/routers/ws";
|
||||||
|
|
||||||
|
|||||||
@@ -98,19 +98,3 @@ export async function getValidCertificatesForDomains(
|
|||||||
|
|
||||||
return validCertsDecrypted;
|
return validCertsDecrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getValidCertificatesForDomainsHybrid(
|
|
||||||
domains: Set<string>
|
|
||||||
): Promise<
|
|
||||||
Array<{
|
|
||||||
id: number;
|
|
||||||
domain: string;
|
|
||||||
wildcard: boolean | null;
|
|
||||||
certFile: string | null;
|
|
||||||
keyFile: string | null;
|
|
||||||
expiresAt: number | null;
|
|
||||||
updatedAt?: number | null;
|
|
||||||
}>
|
|
||||||
> {
|
|
||||||
return []; // stub
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -146,6 +146,10 @@ export class PrivateConfig {
|
|||||||
if (parsedPrivateConfig.stripe?.s3Region) {
|
if (parsedPrivateConfig.stripe?.s3Region) {
|
||||||
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
process.env.S3_REGION = parsedPrivateConfig.stripe.s3Region;
|
||||||
}
|
}
|
||||||
|
if (parsedPrivateConfig.flags?.generate_own_certificates) {
|
||||||
|
process.env.GENERATE_OWN_CERTIFICATES =
|
||||||
|
parsedPrivateConfig.flags.generate_own_certificates.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.rawPrivateConfig = parsedPrivateConfig;
|
this.rawPrivateConfig = parsedPrivateConfig;
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import redisManager from "@server/private/lib/redis";
|
import redisManager from "#private/lib/redis";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
// Rate limiting configuration
|
// Rate limiting configuration
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { MemoryStore, Store } from "express-rate-limit";
|
|||||||
import RedisStore from "#private/lib/redisStore";
|
import RedisStore from "#private/lib/redisStore";
|
||||||
|
|
||||||
export function createStore(): Store {
|
export function createStore(): Store {
|
||||||
if (build != "oss" && privateConfig.getRawPrivateConfig().flags?.enable_redis) {
|
if (build != "oss" && privateConfig.getRawPrivateConfig().flags.enable_redis) {
|
||||||
const rateLimitStore: Store = new RedisStore({
|
const rateLimitStore: Store = new RedisStore({
|
||||||
prefix: "api-rate-limit", // Optional: customize Redis key prefix
|
prefix: "api-rate-limit", // Optional: customize Redis key prefix
|
||||||
skipFailedRequests: true, // Don't count failed requests
|
skipFailedRequests: true, // Don't count failed requests
|
||||||
|
|||||||
@@ -20,15 +20,18 @@ import { build } from "@server/build";
|
|||||||
|
|
||||||
const portSchema = z.number().positive().gt(0).lte(65535);
|
const portSchema = z.number().positive().gt(0).lte(65535);
|
||||||
|
|
||||||
export const privateConfigSchema = z
|
export const privateConfigSchema = z.object({
|
||||||
.object({
|
app: z
|
||||||
app: z.object({
|
.object({
|
||||||
region: z.string().optional().default("default"),
|
region: z.string().optional().default("default"),
|
||||||
base_domain: z.string().optional()
|
base_domain: z.string().optional()
|
||||||
}).optional().default({
|
})
|
||||||
|
.optional()
|
||||||
|
.default({
|
||||||
region: "default"
|
region: "default"
|
||||||
}),
|
}),
|
||||||
server: z.object({
|
server: z
|
||||||
|
.object({
|
||||||
encryption_key_path: z
|
encryption_key_path: z
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.optional()
|
||||||
@@ -37,125 +40,132 @@ export const privateConfigSchema = z
|
|||||||
resend_api_key: z.string().optional(),
|
resend_api_key: z.string().optional(),
|
||||||
reo_client_id: z.string().optional(),
|
reo_client_id: z.string().optional(),
|
||||||
fossorial_api_key: z.string().optional()
|
fossorial_api_key: z.string().optional()
|
||||||
}).optional().default({
|
})
|
||||||
|
.optional()
|
||||||
|
.default({
|
||||||
encryption_key_path: "./config/encryption.pem"
|
encryption_key_path: "./config/encryption.pem"
|
||||||
}),
|
}),
|
||||||
redis: z
|
redis: z
|
||||||
.object({
|
.object({
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0),
|
db: z.number().int().nonnegative().optional().default(0),
|
||||||
replicas: z
|
replicas: z
|
||||||
.array(
|
.array(
|
||||||
z.object({
|
z.object({
|
||||||
host: z.string(),
|
host: z.string(),
|
||||||
port: portSchema,
|
port: portSchema,
|
||||||
password: z.string().optional(),
|
password: z.string().optional(),
|
||||||
db: z.number().int().nonnegative().optional().default(0)
|
db: z.number().int().nonnegative().optional().default(0)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
// tls: z
|
||||||
|
// .object({
|
||||||
|
// reject_unauthorized: z
|
||||||
|
// .boolean()
|
||||||
|
// .optional()
|
||||||
|
// .default(true)
|
||||||
|
// })
|
||||||
|
// .optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
gerbil: z
|
||||||
|
.object({
|
||||||
|
local_exit_node_reachable_at: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.default("http://gerbil:3003")
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({}),
|
||||||
|
flags: z
|
||||||
|
.object({
|
||||||
|
enable_redis: z.boolean().optional().default(false),
|
||||||
|
generate_own_certificates: z.boolean().optional().default(false)
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.default({}),
|
||||||
|
branding: z
|
||||||
|
.object({
|
||||||
|
app_name: z.string().optional(),
|
||||||
|
background_image_path: z.string().optional(),
|
||||||
|
colors: z
|
||||||
|
.object({
|
||||||
|
light: colorsSchema.optional(),
|
||||||
|
dark: colorsSchema.optional()
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
logo: z
|
||||||
|
.object({
|
||||||
|
light_path: z.string().optional(),
|
||||||
|
dark_path: z.string().optional(),
|
||||||
|
auth_page: z
|
||||||
|
.object({
|
||||||
|
width: z.number().optional(),
|
||||||
|
height: z.number().optional()
|
||||||
})
|
})
|
||||||
)
|
.optional(),
|
||||||
.optional()
|
navbar: z
|
||||||
// tls: z
|
.object({
|
||||||
// .object({
|
width: z.number().optional(),
|
||||||
// reject_unauthorized: z
|
height: z.number().optional()
|
||||||
// .boolean()
|
|
||||||
// .optional()
|
|
||||||
// .default(true)
|
|
||||||
// })
|
|
||||||
// .optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
gerbil: z
|
|
||||||
.object({
|
|
||||||
local_exit_node_reachable_at: z.string().optional().default("http://gerbil:3003")
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.default({}),
|
|
||||||
flags: z
|
|
||||||
.object({
|
|
||||||
enable_redis: z.boolean().optional(),
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
branding: z
|
|
||||||
.object({
|
|
||||||
app_name: z.string().optional(),
|
|
||||||
background_image_path: z.string().optional(),
|
|
||||||
colors: z
|
|
||||||
.object({
|
|
||||||
light: colorsSchema.optional(),
|
|
||||||
dark: colorsSchema.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
logo: z
|
|
||||||
.object({
|
|
||||||
light_path: z.string().optional(),
|
|
||||||
dark_path: z.string().optional(),
|
|
||||||
auth_page: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
navbar: z
|
|
||||||
.object({
|
|
||||||
width: z.number().optional(),
|
|
||||||
height: z.number().optional()
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
})
|
|
||||||
.optional(),
|
|
||||||
favicon_path: z.string().optional(),
|
|
||||||
footer: z
|
|
||||||
.array(
|
|
||||||
z.object({
|
|
||||||
text: z.string(),
|
|
||||||
href: z.string().optional()
|
|
||||||
})
|
})
|
||||||
)
|
.optional()
|
||||||
.optional(),
|
})
|
||||||
login_page: z
|
.optional(),
|
||||||
.object({
|
favicon_path: z.string().optional(),
|
||||||
subtitle_text: z.string().optional(),
|
footer: z
|
||||||
title_text: z.string().optional()
|
.array(
|
||||||
|
z.object({
|
||||||
|
text: z.string(),
|
||||||
|
href: z.string().optional()
|
||||||
})
|
})
|
||||||
.optional(),
|
)
|
||||||
signup_page: z
|
.optional(),
|
||||||
.object({
|
login_page: z
|
||||||
subtitle_text: z.string().optional(),
|
.object({
|
||||||
title_text: z.string().optional()
|
subtitle_text: z.string().optional(),
|
||||||
})
|
title_text: z.string().optional()
|
||||||
.optional(),
|
})
|
||||||
resource_auth_page: z
|
.optional(),
|
||||||
.object({
|
signup_page: z
|
||||||
show_logo: z.boolean().optional(),
|
.object({
|
||||||
hide_powered_by: z.boolean().optional(),
|
subtitle_text: z.string().optional(),
|
||||||
title_text: z.string().optional(),
|
title_text: z.string().optional()
|
||||||
subtitle_text: z.string().optional()
|
})
|
||||||
})
|
.optional(),
|
||||||
.optional(),
|
resource_auth_page: z
|
||||||
emails: z
|
.object({
|
||||||
.object({
|
show_logo: z.boolean().optional(),
|
||||||
signature: z.string().optional(),
|
hide_powered_by: z.boolean().optional(),
|
||||||
colors: z
|
title_text: z.string().optional(),
|
||||||
.object({
|
subtitle_text: z.string().optional()
|
||||||
primary: z.string().optional()
|
})
|
||||||
})
|
.optional(),
|
||||||
.optional()
|
emails: z
|
||||||
})
|
.object({
|
||||||
.optional()
|
signature: z.string().optional(),
|
||||||
})
|
colors: z
|
||||||
.optional(),
|
.object({
|
||||||
stripe: z
|
primary: z.string().optional()
|
||||||
.object({
|
})
|
||||||
secret_key: z.string(),
|
.optional()
|
||||||
webhook_secret: z.string(),
|
})
|
||||||
s3Bucket: z.string(),
|
.optional()
|
||||||
s3Region: z.string().default("us-east-1"),
|
})
|
||||||
localFilePath: z.string()
|
.optional(),
|
||||||
})
|
stripe: z
|
||||||
.optional(),
|
.object({
|
||||||
});
|
secret_key: z.string(),
|
||||||
|
webhook_secret: z.string(),
|
||||||
|
s3Bucket: z.string(),
|
||||||
|
s3Region: z.string().default("us-east-1"),
|
||||||
|
localFilePath: z.string()
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
});
|
||||||
|
|
||||||
export function readPrivateConfigFile() {
|
export function readPrivateConfigFile() {
|
||||||
if (build == "oss") {
|
if (build == "oss") {
|
||||||
@@ -186,9 +196,7 @@ export function readPrivateConfigFile() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!environment) {
|
if (!environment) {
|
||||||
throw new Error(
|
throw new Error("No private configuration file found.");
|
||||||
"No private configuration file found."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return environment;
|
return environment;
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class RedisManager {
|
|||||||
this.isEnabled = false;
|
this.isEnabled = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.isEnabled = privateConfig.getRawPrivateConfig().flags?.enable_redis || false;
|
this.isEnabled = privateConfig.getRawPrivateConfig().flags.enable_redis || false;
|
||||||
if (this.isEnabled) {
|
if (this.isEnabled) {
|
||||||
this.initializeClients();
|
this.initializeClients();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,10 +14,10 @@
|
|||||||
import Stripe from "stripe";
|
import Stripe from "stripe";
|
||||||
import privateConfig from "#private/lib/config";
|
import privateConfig from "#private/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { build } from "@server/build";
|
import { noop } from "@server/lib/billing/usageService";
|
||||||
|
|
||||||
let stripe: Stripe | undefined = undefined;
|
let stripe: Stripe | undefined = undefined;
|
||||||
if (build == "saas") {
|
if (!noop()) {
|
||||||
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
|
const stripeApiKey = privateConfig.getRawPrivateConfig().stripe?.secret_key;
|
||||||
if (!stripeApiKey) {
|
if (!stripeApiKey) {
|
||||||
logger.error("Stripe secret key is not configured");
|
logger.error("Stripe secret key is not configured");
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ import {
|
|||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
import { and, eq, inArray, or, isNull, ne, isNotNull, desc } from "drizzle-orm";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { orgs, resources, sites, Target, targets } from "@server/db";
|
import { orgs, resources, sites, Target, targets } from "@server/db";
|
||||||
import { build } from "@server/build";
|
|
||||||
import { sanitize } from "@server/lib/traefik/utils";
|
import { sanitize } from "@server/lib/traefik/utils";
|
||||||
|
import privateConfig from "#private/lib/config";
|
||||||
|
|
||||||
const redirectHttpsMiddlewareName = "redirect-to-https";
|
const redirectHttpsMiddlewareName = "redirect-to-https";
|
||||||
const redirectToRootMiddlewareName = "redirect-to-root";
|
const redirectToRootMiddlewareName = "redirect-to-root";
|
||||||
@@ -234,12 +233,13 @@ export async function getTraefikConfig(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resource.certificateStatus !== "valid") {
|
// TODO: for now dont filter it out because if you have multiple domain ids and one is failed it causes all of them to fail
|
||||||
logger.debug(
|
// if (resource.certificateStatus !== "valid" && privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||||
`Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
// logger.debug(
|
||||||
);
|
// `Resource ${resource.resourceId} has certificate stats ${resource.certificateStats}`
|
||||||
continue;
|
// );
|
||||||
}
|
// continue;
|
||||||
|
// }
|
||||||
|
|
||||||
// add routers and services empty objects if they don't exist
|
// add routers and services empty objects if they don't exist
|
||||||
if (!config_output.http.routers) {
|
if (!config_output.http.routers) {
|
||||||
@@ -264,18 +264,21 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
const configDomain = config.getDomain(resource.domainId);
|
const configDomain = config.getDomain(resource.domainId);
|
||||||
|
|
||||||
let certResolver: string, preferWildcardCert: boolean;
|
|
||||||
if (!configDomain) {
|
|
||||||
certResolver = config.getRawConfig().traefik.cert_resolver;
|
|
||||||
preferWildcardCert =
|
|
||||||
config.getRawConfig().traefik.prefer_wildcard_cert;
|
|
||||||
} else {
|
|
||||||
certResolver = configDomain.cert_resolver;
|
|
||||||
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
|
||||||
}
|
|
||||||
|
|
||||||
let tls = {};
|
let tls = {};
|
||||||
if (build == "oss") {
|
if (
|
||||||
|
!privateConfig.getRawPrivateConfig().flags
|
||||||
|
.generate_own_certificates
|
||||||
|
) {
|
||||||
|
let certResolver: string, preferWildcardCert: boolean;
|
||||||
|
if (!configDomain) {
|
||||||
|
certResolver = config.getRawConfig().traefik.cert_resolver;
|
||||||
|
preferWildcardCert =
|
||||||
|
config.getRawConfig().traefik.prefer_wildcard_cert;
|
||||||
|
} else {
|
||||||
|
certResolver = configDomain.cert_resolver;
|
||||||
|
preferWildcardCert = configDomain.prefer_wildcard_cert;
|
||||||
|
}
|
||||||
|
|
||||||
tls = {
|
tls = {
|
||||||
certResolver: certResolver,
|
certResolver: certResolver,
|
||||||
...(preferWildcardCert
|
...(preferWildcardCert
|
||||||
@@ -419,7 +422,7 @@ export async function getTraefikConfig(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
(targets as TargetWithSite[])
|
(targets as TargetWithSite[])
|
||||||
.filter((target: TargetWithSite) => {
|
.filter((target: TargetWithSite) => {
|
||||||
if (!target.enabled) {
|
if (!target.enabled) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -440,7 +443,7 @@ export async function getTraefikConfig(
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (target.site.type === "newt") {
|
} else if (target.site.type === "newt") {
|
||||||
if (
|
if (
|
||||||
!target.internalPort ||
|
!target.internalPort ||
|
||||||
!target.method ||
|
!target.method ||
|
||||||
@@ -448,10 +451,10 @@ export async function getTraefikConfig(
|
|||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.map((target: TargetWithSite) => {
|
.map((target: TargetWithSite) => {
|
||||||
if (
|
if (
|
||||||
target.site.type === "local" ||
|
target.site.type === "local" ||
|
||||||
target.site.type === "wireguard"
|
target.site.type === "wireguard"
|
||||||
@@ -459,14 +462,14 @@ export async function getTraefikConfig(
|
|||||||
return {
|
return {
|
||||||
url: `${target.method}://${target.ip}:${target.port}`
|
url: `${target.method}://${target.ip}:${target.port}`
|
||||||
};
|
};
|
||||||
} else if (target.site.type === "newt") {
|
} else if (target.site.type === "newt") {
|
||||||
const ip =
|
const ip =
|
||||||
target.site.subnet!.split("/")[0];
|
target.site.subnet!.split("/")[0];
|
||||||
return {
|
return {
|
||||||
url: `${target.method}://${ip}:${target.internalPort}`
|
url: `${target.method}://${ip}:${target.internalPort}`
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// filter out duplicates
|
// filter out duplicates
|
||||||
.filter(
|
.filter(
|
||||||
(v, i, a) =>
|
(v, i, a) =>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { sha256 } from "@oslojs/crypto/sha2";
|
|||||||
import { serializeSessionCookie } from "@server/auth/sessions/app";
|
import { serializeSessionCookie } from "@server/auth/sessions/app";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
|
import { TransferSessionResponse } from "@server/routers/auth/types";
|
||||||
|
|
||||||
const bodySchema = z.object({
|
const bodySchema = z.object({
|
||||||
token: z.string()
|
token: z.string()
|
||||||
@@ -33,11 +34,6 @@ const bodySchema = z.object({
|
|||||||
|
|
||||||
export type TransferSessionBodySchema = z.infer<typeof bodySchema>;
|
export type TransferSessionBodySchema = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
export type TransferSessionResponse = {
|
|
||||||
valid: boolean;
|
|
||||||
cookie?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function transferSession(
|
export async function transferSession(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||||
|
|
||||||
// Import tables for billing
|
// Import tables for billing
|
||||||
import {
|
import {
|
||||||
customers,
|
customers,
|
||||||
@@ -37,11 +39,6 @@ const getOrgSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetOrgSubscriptionResponse = {
|
|
||||||
subscription: Subscription | null;
|
|
||||||
items: SubscriptionItem[];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/billing/subscription",
|
path: "/org/{orgId}/billing/subscription",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
import { Limit, limits, Usage, usage } from "@server/db";
|
import { Limit, limits, Usage, usage } from "@server/db";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
|
import { GetOrgUsageResponse } from "@server/routers/billing/types";
|
||||||
|
|
||||||
const getOrgSchema = z
|
const getOrgSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -32,11 +33,6 @@ const getOrgSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetOrgUsageResponse = {
|
|
||||||
usage: Usage[];
|
|
||||||
limits: Limit[];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/billing/usage",
|
path: "/org/{orgId}/billing/usage",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromZodError } from "zod-validation-error";
|
import { fromZodError } from "zod-validation-error";
|
||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
|
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||||
|
|
||||||
const getOrgSchema = z
|
const getOrgSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -26,11 +27,6 @@ const getOrgSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type GetOrgTierResponse = {
|
|
||||||
tier: string | null;
|
|
||||||
active: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function getOrgTier(
|
export async function getOrgTier(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -15,15 +15,19 @@ import { Certificate, certificates, db, domains } from "@server/db";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { Transaction } from "@server/db";
|
import { Transaction } from "@server/db";
|
||||||
import { eq, or, and, like } from "drizzle-orm";
|
import { eq, or, and, like } from "drizzle-orm";
|
||||||
import { build } from "@server/build";
|
import privateConfig from "#private/lib/config";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if a certificate exists for the given domain.
|
* Checks if a certificate exists for the given domain.
|
||||||
* If not, creates a new certificate in 'pending' state.
|
* If not, creates a new certificate in 'pending' state.
|
||||||
* Wildcard certs cover subdomains.
|
* Wildcard certs cover subdomains.
|
||||||
*/
|
*/
|
||||||
export async function createCertificate(domainId: string, domain: string, trx: Transaction | typeof db) {
|
export async function createCertificate(
|
||||||
if (build !== "saas") {
|
domainId: string,
|
||||||
|
domain: string,
|
||||||
|
trx: Transaction | typeof db
|
||||||
|
) {
|
||||||
|
if (!privateConfig.getRawPrivateConfig().flags.generate_own_certificates) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
|
|
||||||
let existing: Certificate[] = [];
|
let existing: Certificate[] = [];
|
||||||
if (domainRecord.type == "ns") {
|
if (domainRecord.type == "ns") {
|
||||||
const domainLevelDown = domain.split('.').slice(1).join('.');
|
const domainLevelDown = domain.split(".").slice(1).join(".");
|
||||||
existing = await trx
|
existing = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(certificates)
|
.from(certificates)
|
||||||
@@ -49,7 +53,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
eq(certificates.wildcard, true), // only NS domains can have wildcard certs
|
||||||
or(
|
or(
|
||||||
eq(certificates.domain, domain),
|
eq(certificates.domain, domain),
|
||||||
eq(certificates.domain, domainLevelDown),
|
eq(certificates.domain, domainLevelDown)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
@@ -67,9 +71,7 @@ export async function createCertificate(domainId: string, domain: string, trx: T
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (existing.length > 0) {
|
if (existing.length > 0) {
|
||||||
logger.info(
|
logger.info(`Certificate already exists for domain ${domain}`);
|
||||||
`Certificate already exists for domain ${domain}`
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { registry } from "@server/openApi";
|
import { registry } from "@server/openApi";
|
||||||
|
import { GetCertificateResponse } from "@server/routers/certificates/types";
|
||||||
|
|
||||||
const getCertificateSchema = z
|
const getCertificateSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -96,20 +97,6 @@ async function query(domainId: string, domain: string) {
|
|||||||
return existing.length > 0 ? existing[0] : null;
|
return existing.length > 0 ? existing[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetCertificateResponse = {
|
|
||||||
certId: number;
|
|
||||||
domain: string;
|
|
||||||
domainId: string;
|
|
||||||
wildcard: boolean;
|
|
||||||
status: string; // pending, requested, valid, expired, failed
|
|
||||||
expiresAt: string | null;
|
|
||||||
lastRenewalAttempt: Date | null;
|
|
||||||
createdAt: string;
|
|
||||||
updatedAt: string;
|
|
||||||
errorMessage?: string | null;
|
|
||||||
renewalCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/certificate/{domainId}/{domain}",
|
path: "/org/{orgId}/certificate/{domainId}/{domain}",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { fromError } from "zod-validation-error";
|
|||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { db, domainNamespaces, resources } from "@server/db";
|
import { db, domainNamespaces, resources } from "@server/db";
|
||||||
import { inArray } from "drizzle-orm";
|
import { inArray } from "drizzle-orm";
|
||||||
|
import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
|
||||||
|
|
||||||
const paramsSchema = z.object({}).strict();
|
const paramsSchema = z.object({}).strict();
|
||||||
|
|
||||||
@@ -30,15 +31,6 @@ const querySchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type CheckDomainAvailabilityResponse = {
|
|
||||||
available: boolean;
|
|
||||||
options: {
|
|
||||||
domainNamespaceId: string;
|
|
||||||
domainId: string;
|
|
||||||
fullDomain: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/domain/check-namespace-availability",
|
path: "/domain/check-namespace-availability",
|
||||||
|
|||||||
@@ -39,16 +39,17 @@ import {
|
|||||||
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
import rateLimit, { ipKeyGenerator } from "express-rate-limit";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
|
||||||
import {
|
|
||||||
unauthenticated as ua,
|
|
||||||
authenticated as a
|
|
||||||
} from "@server/routers/external";
|
|
||||||
import { verifyValidLicense } from "../middlewares/verifyValidLicense";
|
import { verifyValidLicense } from "../middlewares/verifyValidLicense";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import {
|
||||||
|
unauthenticated as ua,
|
||||||
|
authenticated as a,
|
||||||
|
authRouter as aa
|
||||||
|
} from "@server/routers/external";
|
||||||
|
|
||||||
export const authenticated = a;
|
export const authenticated = a;
|
||||||
export const unauthenticated = ua;
|
export const unauthenticated = ua;
|
||||||
|
export const authRouter = aa;
|
||||||
|
|
||||||
unauthenticated.post(
|
unauthenticated.post(
|
||||||
"/remote-exit-node/quick-start",
|
"/remote-exit-node/quick-start",
|
||||||
@@ -276,8 +277,6 @@ authenticated.get(
|
|||||||
loginPage.getLoginPage
|
loginPage.getLoginPage
|
||||||
);
|
);
|
||||||
|
|
||||||
export const authRouter = Router();
|
|
||||||
|
|
||||||
authRouter.post(
|
authRouter.post(
|
||||||
"/remoteExitNode/get-token",
|
"/remoteExitNode/get-token",
|
||||||
verifyValidLicense,
|
verifyValidLicense,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ import { exchangeSession } from "@server/routers/badger";
|
|||||||
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
import { validateResourceSessionToken } from "@server/auth/sessions/resource";
|
||||||
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
import { checkExitNodeOrg, resolveExitNodes } from "#private/lib/exitNodes";
|
||||||
import { maxmindLookup } from "@server/db/maxmind";
|
import { maxmindLookup } from "@server/db/maxmind";
|
||||||
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
|
|
||||||
// Zod schemas for request validation
|
// Zod schemas for request validation
|
||||||
const getResourceByDomainParamsSchema = z
|
const getResourceByDomainParamsSchema = z
|
||||||
@@ -162,6 +163,14 @@ const validateResourceSessionTokenBodySchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
|
const validateResourceAccessTokenBodySchema = z
|
||||||
|
.object({
|
||||||
|
accessTokenId: z.string().optional(),
|
||||||
|
resourceId: z.number().optional(),
|
||||||
|
accessToken: z.string()
|
||||||
|
})
|
||||||
|
.strict();
|
||||||
|
|
||||||
// Certificates by domains query validation
|
// Certificates by domains query validation
|
||||||
const getCertificatesByDomainsQuerySchema = z
|
const getCertificatesByDomainsQuerySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -215,6 +224,33 @@ export type UserSessionWithUser = {
|
|||||||
export const hybridRouter = Router();
|
export const hybridRouter = Router();
|
||||||
hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
|
hybridRouter.use(verifySessionRemoteExitNodeMiddleware);
|
||||||
|
|
||||||
|
hybridRouter.get(
|
||||||
|
"/general-config",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
resource_session_request_param:
|
||||||
|
config.getRawConfig().server.resource_session_request_param,
|
||||||
|
resource_access_token_headers:
|
||||||
|
config.getRawConfig().server.resource_access_token_headers,
|
||||||
|
resource_access_token_param:
|
||||||
|
config.getRawConfig().server.resource_access_token_param,
|
||||||
|
session_cookie_name:
|
||||||
|
config.getRawConfig().server.session_cookie_name,
|
||||||
|
require_email_verification:
|
||||||
|
config.getRawConfig().flags?.require_email_verification ||
|
||||||
|
false,
|
||||||
|
resource_session_length_hours:
|
||||||
|
config.getRawConfig().server.resource_session_length_hours
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "General config retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
hybridRouter.get(
|
hybridRouter.get(
|
||||||
"/traefik-config",
|
"/traefik-config",
|
||||||
async (req: Request, res: Response, next: NextFunction) => {
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
@@ -1101,6 +1137,52 @@ hybridRouter.post(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Validate resource session token
|
||||||
|
hybridRouter.post(
|
||||||
|
"/resource/:resourceId/access-token/verify",
|
||||||
|
async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const parsedBody = validateResourceAccessTokenBodySchema.safeParse(
|
||||||
|
req.body
|
||||||
|
);
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { accessToken, resourceId, accessTokenId } = parsedBody.data;
|
||||||
|
|
||||||
|
const result = await verifyResourceAccessToken({
|
||||||
|
accessTokenId,
|
||||||
|
accessToken,
|
||||||
|
resourceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return response(res, {
|
||||||
|
data: result,
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: result.valid
|
||||||
|
? "Resource access token is valid"
|
||||||
|
: "Resource access token is invalid or expired",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to validate resource session token"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const geoIpLookupParamsSchema = z.object({
|
const geoIpLookupParamsSchema = z.object({
|
||||||
ip: z.string().ip()
|
ip: z.string().ip()
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import { createCertificate } from "#private/routers/certificates/createCertifica
|
|||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { CreateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -49,8 +50,6 @@ const bodySchema = z
|
|||||||
|
|
||||||
export type CreateLoginPageBody = z.infer<typeof bodySchema>;
|
export type CreateLoginPageBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
export type CreateLoginPageResponse = LoginPage;
|
|
||||||
|
|
||||||
export async function createLoginPage(
|
export async function createLoginPage(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
|
import { DeleteLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -28,8 +29,6 @@ const paramsSchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type DeleteLoginPageResponse = LoginPage;
|
|
||||||
|
|
||||||
export async function deleteLoginPage(
|
export async function deleteLoginPage(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -40,10 +41,6 @@ async function query(orgId: string) {
|
|||||||
return res?.loginPage;
|
return res?.loginPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetLoginPageResponse = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof query>>
|
|
||||||
>;
|
|
||||||
|
|
||||||
export async function getLoginPage(
|
export async function getLoginPage(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
resourceId: z.coerce.number().int().positive().optional(),
|
resourceId: z.coerce.number().int().positive().optional(),
|
||||||
@@ -70,10 +71,6 @@ async function query(orgId: string | undefined, fullDomain: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LoadLoginPageResponse = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof query>>
|
|
||||||
> & { orgId: string };
|
|
||||||
|
|
||||||
export async function loadLoginPage(
|
export async function loadLoginPage(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import { createCertificate } from "#private/routers/certificates/createCertifica
|
|||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
import { UpdateLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -55,8 +56,6 @@ const bodySchema = z
|
|||||||
|
|
||||||
export type UpdateLoginPageBody = z.infer<typeof bodySchema>;
|
export type UpdateLoginPageBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
export type UpdateLoginPageResponse = LoginPage;
|
|
||||||
|
|
||||||
export async function updateLoginPage(
|
export async function updateLoginPage(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import config from "@server/lib/config";
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { getOrgTierData } from "#private/lib/billing";
|
import { getOrgTierData } from "#private/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
|
import { CreateOrgIdpResponse } from "@server/routers/orgIdp/types";
|
||||||
|
|
||||||
const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict();
|
const paramsSchema = z.object({ orgId: z.string().nonempty() }).strict();
|
||||||
|
|
||||||
@@ -47,11 +48,6 @@ const bodySchema = z
|
|||||||
})
|
})
|
||||||
.strict();
|
.strict();
|
||||||
|
|
||||||
export type CreateOrgIdpResponse = {
|
|
||||||
idpId: number;
|
|
||||||
redirectUrl: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// registry.registerPath({
|
// registry.registerPath({
|
||||||
// method: "put",
|
// method: "put",
|
||||||
// path: "/idp/oidc",
|
// path: "/idp/oidc",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import { OpenAPITags, registry } from "@server/openApi";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { decrypt } from "@server/lib/crypto";
|
import { decrypt } from "@server/lib/crypto";
|
||||||
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
import { generateOidcRedirectUrl } from "@server/lib/idp/generateRedirectUrl";
|
||||||
|
import { GetOrgIdpResponse } from "@server/routers/orgIdp/types";
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -47,10 +48,6 @@ async function query(idpId: number, orgId: string) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetOrgIdpResponse = NonNullable<
|
|
||||||
Awaited<ReturnType<typeof query>>
|
|
||||||
> & { redirectUrl: string };
|
|
||||||
|
|
||||||
// registry.registerPath({
|
// registry.registerPath({
|
||||||
// method: "get",
|
// method: "get",
|
||||||
// path: "/idp/{idpId}",
|
// path: "/idp/{idpId}",
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { eq, sql } from "drizzle-orm";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
|
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
||||||
|
|
||||||
const querySchema = z
|
const querySchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -65,15 +66,6 @@ async function query(orgId: string, limit: number, offset: number) {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListOrgIdpsResponse = {
|
|
||||||
idps: Awaited<ReturnType<typeof query>>;
|
|
||||||
pagination: {
|
|
||||||
total: number;
|
|
||||||
limit: number;
|
|
||||||
offset: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// registry.registerPath({
|
// registry.registerPath({
|
||||||
// method: "get",
|
// method: "get",
|
||||||
// path: "/idp",
|
// path: "/idp",
|
||||||
|
|||||||
@@ -29,17 +29,12 @@ import { and, eq } from "drizzle-orm";
|
|||||||
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
|
import { getNextAvailableSubnet } from "@server/lib/exitNodes";
|
||||||
import { usageService } from "@server/lib/billing/usageService";
|
import { usageService } from "@server/lib/billing/usageService";
|
||||||
import { FeatureId } from "@server/lib/billing";
|
import { FeatureId } from "@server/lib/billing";
|
||||||
|
import { CreateRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
|
|
||||||
export const paramsSchema = z.object({
|
export const paramsSchema = z.object({
|
||||||
orgId: z.string()
|
orgId: z.string()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type CreateRemoteExitNodeResponse = {
|
|
||||||
token: string;
|
|
||||||
remoteExitNodeId: string;
|
|
||||||
secret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bodySchema = z
|
const bodySchema = z
|
||||||
.object({
|
.object({
|
||||||
remoteExitNodeId: z.string().length(15),
|
remoteExitNodeId: z.string().length(15),
|
||||||
@@ -89,30 +84,25 @@ export async function createRemoteExitNode(
|
|||||||
orgId,
|
orgId,
|
||||||
FeatureId.REMOTE_EXIT_NODES
|
FeatureId.REMOTE_EXIT_NODES
|
||||||
);
|
);
|
||||||
if (!usage) {
|
if (usage) {
|
||||||
return next(
|
const rejectRemoteExitNodes = await usageService.checkLimitSet(
|
||||||
createHttpError(
|
orgId,
|
||||||
HttpCode.NOT_FOUND,
|
false,
|
||||||
"No usage data found for this organization"
|
FeatureId.REMOTE_EXIT_NODES,
|
||||||
)
|
{
|
||||||
);
|
...usage,
|
||||||
}
|
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
||||||
const rejectRemoteExitNodes = await usageService.checkLimitSet(
|
} // We need to add one to know if we are violating the limit
|
||||||
orgId,
|
|
||||||
false,
|
|
||||||
FeatureId.REMOTE_EXIT_NODES,
|
|
||||||
{
|
|
||||||
...usage,
|
|
||||||
instantaneousValue: (usage.instantaneousValue || 0) + 1
|
|
||||||
} // We need to add one to know if we are violating the limit
|
|
||||||
);
|
|
||||||
if (rejectRemoteExitNodes) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.FORBIDDEN,
|
|
||||||
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (rejectRemoteExitNodes) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.FORBIDDEN,
|
||||||
|
"Remote exit node limit exceeded. Please upgrade your plan or contact us at support@fossorial.io"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretHash = await hashPassword(secret);
|
const secretHash = await hashPassword(secret);
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
|
|
||||||
const getRemoteExitNodeSchema = z
|
const getRemoteExitNodeSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -52,8 +53,6 @@ async function query(remoteExitNodeId: string) {
|
|||||||
return remoteExitNode;
|
return remoteExitNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type GetRemoteExitNodeResponse = Awaited<ReturnType<typeof query>>;
|
|
||||||
|
|
||||||
export async function getRemoteExitNode(
|
export async function getRemoteExitNode(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ export const remoteExitNodeGetTokenBodySchema = z.object({
|
|||||||
token: z.string().optional()
|
token: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RemoteExitNodeGetTokenBody = z.infer<typeof remoteExitNodeGetTokenBodySchema>;
|
|
||||||
|
|
||||||
export async function getRemoteExitNodeToken(
|
export async function getRemoteExitNodeToken(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import HttpCode from "@server/types/HttpCode";
|
|||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||||
|
|
||||||
const listRemoteExitNodesParamsSchema = z
|
const listRemoteExitNodesParamsSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -43,7 +44,7 @@ const listRemoteExitNodesSchema = z.object({
|
|||||||
.pipe(z.number().int().nonnegative())
|
.pipe(z.number().int().nonnegative())
|
||||||
});
|
});
|
||||||
|
|
||||||
function queryRemoteExitNodes(orgId: string) {
|
export function queryRemoteExitNodes(orgId: string) {
|
||||||
return db
|
return db
|
||||||
.select({
|
.select({
|
||||||
remoteExitNodeId: remoteExitNodes.remoteExitNodeId,
|
remoteExitNodeId: remoteExitNodes.remoteExitNodeId,
|
||||||
@@ -65,11 +66,6 @@ function queryRemoteExitNodes(orgId: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ListRemoteExitNodesResponse = {
|
|
||||||
remoteExitNodes: Awaited<ReturnType<typeof queryRemoteExitNodes>>;
|
|
||||||
pagination: { total: number; limit: number; offset: number };
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function listRemoteExitNodes(
|
export async function listRemoteExitNodes(
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response,
|
res: Response,
|
||||||
|
|||||||
@@ -19,11 +19,7 @@ import logger from "@server/logger";
|
|||||||
import { generateId } from "@server/auth/sessions/app";
|
import { generateId } from "@server/auth/sessions/app";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { PickRemoteExitNodeDefaultsResponse } from "@server/routers/remoteExitNode/types";
|
||||||
export type PickRemoteExitNodeDefaultsResponse = {
|
|
||||||
remoteExitNodeId: string;
|
|
||||||
secret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const paramsSchema = z
|
const paramsSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextFunction, Request, Response } from "express";
|
import { NextFunction, Request, Response } from "express";
|
||||||
import { db, exitNodes, exitNodeOrgs } from "@server/db";
|
import { db } from "@server/db";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { remoteExitNodes } from "@server/db";
|
import { remoteExitNodes } from "@server/db";
|
||||||
import createHttpError from "http-errors";
|
import createHttpError from "http-errors";
|
||||||
@@ -24,11 +24,7 @@ import { hashPassword } from "@server/auth/password";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
|
import { QuickStartRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
export type QuickStartRemoteExitNodeResponse = {
|
|
||||||
remoteExitNodeId: string;
|
|
||||||
secret: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e";
|
const INSTALLER_KEY = "af4e4785-7e09-11f0-b93a-74563c4e2a7e";
|
||||||
|
|
||||||
|
|||||||
8
server/routers/auth/types.ts
Normal file
8
server/routers/auth/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type TransferSessionResponse = {
|
||||||
|
valid: boolean;
|
||||||
|
cookie?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetSessionTransferTokenRenponse = {
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
@@ -33,7 +33,9 @@ import createHttpError from "http-errors";
|
|||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { getCountryCodeForIp, remoteGetCountryCodeForIp } from "@server/lib/geoip";
|
import {
|
||||||
|
getCountryCodeForIp,
|
||||||
|
} from "@server/lib/geoip";
|
||||||
import { getOrgTierData } from "#dynamic/lib/billing";
|
import { getOrgTierData } from "#dynamic/lib/billing";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { verifyPassword } from "@server/auth/password";
|
import { verifyPassword } from "@server/auth/password";
|
||||||
@@ -106,23 +108,23 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
const clientIp = requestIp
|
const clientIp = requestIp
|
||||||
? (() => {
|
? (() => {
|
||||||
logger.debug("Request IP:", { requestIp });
|
logger.debug("Request IP:", { requestIp });
|
||||||
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
if (requestIp.startsWith("[") && requestIp.includes("]")) {
|
||||||
// if brackets are found, extract the IPv6 address from between the brackets
|
// if brackets are found, extract the IPv6 address from between the brackets
|
||||||
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
const ipv6Match = requestIp.match(/\[(.*?)\]/);
|
||||||
if (ipv6Match) {
|
if (ipv6Match) {
|
||||||
return ipv6Match[1];
|
return ipv6Match[1];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ivp4
|
// ivp4
|
||||||
// split at last colon
|
// split at last colon
|
||||||
const lastColonIndex = requestIp.lastIndexOf(":");
|
const lastColonIndex = requestIp.lastIndexOf(":");
|
||||||
if (lastColonIndex !== -1) {
|
if (lastColonIndex !== -1) {
|
||||||
return requestIp.substring(0, lastColonIndex);
|
return requestIp.substring(0, lastColonIndex);
|
||||||
}
|
}
|
||||||
return requestIp;
|
return requestIp;
|
||||||
})()
|
})()
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
logger.debug("Client IP:", { clientIp });
|
logger.debug("Client IP:", { clientIp });
|
||||||
@@ -137,11 +139,11 @@ export async function verifyResourceSession(
|
|||||||
const resourceCacheKey = `resource:${cleanHost}`;
|
const resourceCacheKey = `resource:${cleanHost}`;
|
||||||
let resourceData:
|
let resourceData:
|
||||||
| {
|
| {
|
||||||
resource: Resource | null;
|
resource: Resource | null;
|
||||||
pincode: ResourcePincode | null;
|
pincode: ResourcePincode | null;
|
||||||
password: ResourcePassword | null;
|
password: ResourcePassword | null;
|
||||||
headerAuth: ResourceHeaderAuth | null;
|
headerAuth: ResourceHeaderAuth | null;
|
||||||
}
|
}
|
||||||
| undefined = cache.get(resourceCacheKey);
|
| undefined = cache.get(resourceCacheKey);
|
||||||
|
|
||||||
if (!resourceData) {
|
if (!resourceData) {
|
||||||
@@ -213,21 +215,21 @@ export async function verifyResourceSession(
|
|||||||
headers &&
|
headers &&
|
||||||
headers[
|
headers[
|
||||||
config.getRawConfig().server.resource_access_token_headers.id
|
config.getRawConfig().server.resource_access_token_headers.id
|
||||||
] &&
|
] &&
|
||||||
headers[
|
headers[
|
||||||
config.getRawConfig().server.resource_access_token_headers.token
|
config.getRawConfig().server.resource_access_token_headers.token
|
||||||
]
|
]
|
||||||
) {
|
) {
|
||||||
const accessTokenId =
|
const accessTokenId =
|
||||||
headers[
|
headers[
|
||||||
config.getRawConfig().server.resource_access_token_headers
|
config.getRawConfig().server.resource_access_token_headers
|
||||||
.id
|
.id
|
||||||
];
|
];
|
||||||
const accessToken =
|
const accessToken =
|
||||||
headers[
|
headers[
|
||||||
config.getRawConfig().server.resource_access_token_headers
|
config.getRawConfig().server.resource_access_token_headers
|
||||||
.token
|
.token
|
||||||
];
|
];
|
||||||
|
|
||||||
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
const { valid, error, tokenItem } = await verifyResourceAccessToken(
|
||||||
{
|
{
|
||||||
@@ -294,10 +296,17 @@ export async function verifyResourceSession(
|
|||||||
|
|
||||||
// check for HTTP Basic Auth header
|
// check for HTTP Basic Auth header
|
||||||
if (headerAuth && clientHeaderAuth) {
|
if (headerAuth && clientHeaderAuth) {
|
||||||
if(cache.get(clientHeaderAuth)) {
|
if (cache.get(clientHeaderAuth)) {
|
||||||
logger.debug("Resource allowed because header auth is valid (cached)");
|
logger.debug(
|
||||||
|
"Resource allowed because header auth is valid (cached)"
|
||||||
|
);
|
||||||
return allowed(res);
|
return allowed(res);
|
||||||
}else if(await verifyPassword(clientHeaderAuth, headerAuth.headerAuthHash)){
|
} else if (
|
||||||
|
await verifyPassword(
|
||||||
|
clientHeaderAuth,
|
||||||
|
headerAuth.headerAuthHash
|
||||||
|
)
|
||||||
|
) {
|
||||||
cache.set(clientHeaderAuth, clientHeaderAuth);
|
cache.set(clientHeaderAuth, clientHeaderAuth);
|
||||||
logger.debug("Resource allowed because header auth is valid");
|
logger.debug("Resource allowed because header auth is valid");
|
||||||
return allowed(res);
|
return allowed(res);
|
||||||
@@ -477,7 +486,11 @@ function extractResourceSessionToken(
|
|||||||
return latest.token;
|
return latest.token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function notAllowed(res: Response, redirectPath?: string, orgId?: string) {
|
async function notAllowed(
|
||||||
|
res: Response,
|
||||||
|
redirectPath?: string,
|
||||||
|
orgId?: string
|
||||||
|
) {
|
||||||
let loginPage: LoginPage | null = null;
|
let loginPage: LoginPage | null = null;
|
||||||
if (orgId) {
|
if (orgId) {
|
||||||
const { tier } = await getOrgTierData(orgId); // returns null in oss
|
const { tier } = await getOrgTierData(orgId); // returns null in oss
|
||||||
@@ -491,14 +504,11 @@ async function notAllowed(res: Response, redirectPath?: string, orgId?: string)
|
|||||||
let endpoint: string;
|
let endpoint: string;
|
||||||
|
|
||||||
if (loginPage && loginPage.domainId && loginPage.fullDomain) {
|
if (loginPage && loginPage.domainId && loginPage.fullDomain) {
|
||||||
const secure = config.getRawConfig().app.dashboard_url?.startsWith("https");
|
const secure = config
|
||||||
|
.getRawConfig()
|
||||||
|
.app.dashboard_url?.startsWith("https");
|
||||||
const method = secure ? "https" : "http";
|
const method = secure ? "https" : "http";
|
||||||
endpoint = `${method}://${loginPage.fullDomain}`;
|
endpoint = `${method}://${loginPage.fullDomain}`;
|
||||||
} else if (config.isManagedMode()) {
|
|
||||||
endpoint =
|
|
||||||
config.getRawConfig().managed?.redirect_endpoint ||
|
|
||||||
config.getRawConfig().managed?.endpoint ||
|
|
||||||
"";
|
|
||||||
} else {
|
} else {
|
||||||
endpoint = config.getRawConfig().app.dashboard_url!;
|
endpoint = config.getRawConfig().app.dashboard_url!;
|
||||||
}
|
}
|
||||||
@@ -803,11 +813,7 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
|
|||||||
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey);
|
||||||
|
|
||||||
if (!cachedCountryCode) {
|
if (!cachedCountryCode) {
|
||||||
if (config.isManagedMode()) {
|
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
||||||
cachedCountryCode = await remoteGetCountryCodeForIp(ip);
|
|
||||||
} else {
|
|
||||||
cachedCountryCode = await getCountryCodeForIp(ip); // do it locally
|
|
||||||
}
|
|
||||||
// Cache for longer since IP geolocation doesn't change frequently
|
// Cache for longer since IP geolocation doesn't change frequently
|
||||||
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes
|
||||||
}
|
}
|
||||||
@@ -817,7 +823,9 @@ async function isIpInGeoIP(ip: string, countryCode: string): Promise<boolean> {
|
|||||||
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
|
return cachedCountryCode?.toUpperCase() === countryCode.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractBasicAuth(headers: Record<string, string> | undefined): string | undefined {
|
function extractBasicAuth(
|
||||||
|
headers: Record<string, string> | undefined
|
||||||
|
): string | undefined {
|
||||||
if (!headers || (!headers.authorization && !headers.Authorization)) {
|
if (!headers || (!headers.authorization && !headers.Authorization)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -833,8 +841,9 @@ function extractBasicAuth(headers: Record<string, string> | undefined): string |
|
|||||||
try {
|
try {
|
||||||
// Extract the base64 encoded credentials
|
// Extract the base64 encoded credentials
|
||||||
return authHeader.slice("Basic ".length);
|
return authHeader.slice("Basic ".length);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug("Basic Auth: Failed to decode credentials", { error: error instanceof Error ? error.message : "Unknown error" });
|
logger.debug("Basic Auth: Failed to decode credentials", {
|
||||||
|
error: error instanceof Error ? error.message : "Unknown error"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
17
server/routers/billing/types.ts
Normal file
17
server/routers/billing/types.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { Limit, Subscription, SubscriptionItem, Usage } from "@server/db";
|
||||||
|
|
||||||
|
export type GetOrgSubscriptionResponse = {
|
||||||
|
subscription: Subscription | null;
|
||||||
|
items: SubscriptionItem[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetOrgUsageResponse = {
|
||||||
|
usage: Usage[];
|
||||||
|
limits: Limit[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetOrgTierResponse = {
|
||||||
|
tier: string | null;
|
||||||
|
active: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
13
server/routers/certificates/types.ts
Normal file
13
server/routers/certificates/types.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
export type GetCertificateResponse = {
|
||||||
|
certId: number;
|
||||||
|
domain: string;
|
||||||
|
domainId: string;
|
||||||
|
wildcard: boolean;
|
||||||
|
status: string; // pending, requested, valid, expired, failed
|
||||||
|
expiresAt: string | null;
|
||||||
|
lastRenewalAttempt: Date | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
errorMessage?: string | null;
|
||||||
|
renewalCount: number;
|
||||||
|
}
|
||||||
8
server/routers/domain/types.ts
Normal file
8
server/routers/domain/types.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
export type CheckDomainAvailabilityResponse = {
|
||||||
|
available: boolean;
|
||||||
|
options: {
|
||||||
|
domainNamespaceId: string;
|
||||||
|
domainId: string;
|
||||||
|
fullDomain: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
@@ -9,7 +9,6 @@ import logger from "@server/logger";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { getAllowedIps } from "../target/helpers";
|
import { getAllowedIps } from "../target/helpers";
|
||||||
import { proxyToRemote } from "@server/lib/remoteProxy";
|
|
||||||
import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
|
import { createExitNode } from "#dynamic/routers/gerbil/createExitNode";
|
||||||
|
|
||||||
// Define Zod schema for request validation
|
// Define Zod schema for request validation
|
||||||
@@ -63,16 +62,6 @@ export async function getConfig(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// STOP HERE IN HYBRID MODE
|
|
||||||
if (config.isManagedMode()) {
|
|
||||||
req.body = {
|
|
||||||
...req.body,
|
|
||||||
endpoint: exitNode.endpoint,
|
|
||||||
listenPort: exitNode.listenPort
|
|
||||||
};
|
|
||||||
return proxyToRemote(req, res, next, "hybrid/gerbil/get-config");
|
|
||||||
}
|
|
||||||
|
|
||||||
const configResponse = await generateGerbilConfig(exitNode);
|
const configResponse = await generateGerbilConfig(exitNode);
|
||||||
|
|
||||||
logger.debug("Sending config: ", configResponse);
|
logger.debug("Sending config: ", configResponse);
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import * as badger from "./badger";
|
|||||||
import * as auth from "@server/routers/auth";
|
import * as auth from "@server/routers/auth";
|
||||||
import * as supporterKey from "@server/routers/supporterKey";
|
import * as supporterKey from "@server/routers/supporterKey";
|
||||||
import * as idp from "@server/routers/idp";
|
import * as idp from "@server/routers/idp";
|
||||||
import { proxyToRemote } from "@server/lib/remoteProxy";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import {
|
import {
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
@@ -48,34 +46,11 @@ internalRouter.get("/idp/:idpId", idp.getIdp);
|
|||||||
const gerbilRouter = Router();
|
const gerbilRouter = Router();
|
||||||
internalRouter.use("/gerbil", gerbilRouter);
|
internalRouter.use("/gerbil", gerbilRouter);
|
||||||
|
|
||||||
if (config.isManagedMode()) {
|
// Use local gerbil endpoints
|
||||||
// Use proxy router to forward requests to remote cloud server
|
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
||||||
// Proxy endpoints for each gerbil route
|
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
|
||||||
gerbilRouter.post("/receive-bandwidth", (req, res, next) =>
|
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
|
||||||
proxyToRemote(req, res, next, "hybrid/gerbil/receive-bandwidth")
|
gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
|
||||||
);
|
|
||||||
|
|
||||||
gerbilRouter.post("/update-hole-punch", (req, res, next) =>
|
|
||||||
proxyToRemote(req, res, next, "hybrid/gerbil/update-hole-punch")
|
|
||||||
);
|
|
||||||
|
|
||||||
gerbilRouter.post("/get-all-relays", (req, res, next) =>
|
|
||||||
proxyToRemote(req, res, next, "hybrid/gerbil/get-all-relays")
|
|
||||||
);
|
|
||||||
|
|
||||||
gerbilRouter.post("/get-resolved-hostname", (req, res, next) =>
|
|
||||||
proxyToRemote(req, res, next, `hybrid/gerbil/get-resolved-hostname`)
|
|
||||||
);
|
|
||||||
|
|
||||||
// GET CONFIG IS HANDLED IN THE ORIGINAL HANDLER
|
|
||||||
// SO IT CAN REGISTER THE LOCAL EXIT NODE
|
|
||||||
} else {
|
|
||||||
// Use local gerbil endpoints
|
|
||||||
gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
|
||||||
gerbilRouter.post("/update-hole-punch", gerbil.updateHolePunch);
|
|
||||||
gerbilRouter.post("/get-all-relays", gerbil.getAllRelays);
|
|
||||||
gerbilRouter.post("/get-resolved-hostname", gerbil.getResolvedHostname);
|
|
||||||
}
|
|
||||||
|
|
||||||
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
|
// WE HANDLE THE PROXY INSIDE OF THIS FUNCTION
|
||||||
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
|
// SO IT REGISTERS THE EXIT NODE LOCALLY AS WELL
|
||||||
@@ -87,10 +62,4 @@ internalRouter.use("/badger", badgerRouter);
|
|||||||
|
|
||||||
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||||
|
|
||||||
if (config.isManagedMode()) {
|
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||||
badgerRouter.post("/exchange-session", (req, res, next) =>
|
|
||||||
proxyToRemote(req, res, next, "hybrid/badger/exchange-session")
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
|
||||||
}
|
|
||||||
|
|||||||
11
server/routers/loginPage/types.ts
Normal file
11
server/routers/loginPage/types.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { LoginPage } from "@server/db";
|
||||||
|
|
||||||
|
export type CreateLoginPageResponse = LoginPage;
|
||||||
|
|
||||||
|
export type DeleteLoginPageResponse = LoginPage;
|
||||||
|
|
||||||
|
export type GetLoginPageResponse = LoginPage;
|
||||||
|
|
||||||
|
export type UpdateLoginPageResponse = LoginPage;
|
||||||
|
|
||||||
|
export type LoadLoginPageResponse = LoginPage & { orgId: string };
|
||||||
27
server/routers/orgIdp/types.ts
Normal file
27
server/routers/orgIdp/types.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { Idp, IdpOidcConfig } from "@server/db";
|
||||||
|
|
||||||
|
export type CreateOrgIdpResponse = {
|
||||||
|
idpId: number;
|
||||||
|
redirectUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetOrgIdpResponse = {
|
||||||
|
idp: Idp,
|
||||||
|
idpOidcConfig: IdpOidcConfig | null,
|
||||||
|
redirectUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ListOrgIdpsResponse = {
|
||||||
|
idps: {
|
||||||
|
idpId: number;
|
||||||
|
orgId: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
variant: string;
|
||||||
|
}[],
|
||||||
|
pagination: {
|
||||||
|
total: number;
|
||||||
|
limit: number;
|
||||||
|
offset: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
34
server/routers/remoteExitNode/types.ts
Normal file
34
server/routers/remoteExitNode/types.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import { RemoteExitNode } from "@server/db";
|
||||||
|
|
||||||
|
export type CreateRemoteExitNodeResponse = {
|
||||||
|
token: string;
|
||||||
|
remoteExitNodeId: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PickRemoteExitNodeDefaultsResponse = {
|
||||||
|
remoteExitNodeId: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type QuickStartRemoteExitNodeResponse = {
|
||||||
|
remoteExitNodeId: string;
|
||||||
|
secret: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ListRemoteExitNodesResponse = {
|
||||||
|
remoteExitNodes: {
|
||||||
|
remoteExitNodeId: string;
|
||||||
|
dateCreated: string;
|
||||||
|
version: string | null;
|
||||||
|
exitNodeId: number | null;
|
||||||
|
name: string;
|
||||||
|
address: string;
|
||||||
|
endpoint: string;
|
||||||
|
online: boolean;
|
||||||
|
type: string | null;
|
||||||
|
}[];
|
||||||
|
pagination: { total: number; limit: number; offset: number };
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetRemoteExitNodeResponse = { remoteExitNodeId: string; dateCreated: string; version: string | null; exitNodeId: number | null; name: string; address: string; endpoint: string; online: boolean; type: string | null; }
|
||||||
@@ -3,7 +3,6 @@ import { eq } from "drizzle-orm";
|
|||||||
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
import { generateRandomString, RandomReader } from "@oslojs/crypto/random";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
|
||||||
|
|
||||||
const random: RandomReader = {
|
const random: RandomReader = {
|
||||||
read(bytes: Uint8Array): void {
|
read(bytes: Uint8Array): void {
|
||||||
@@ -23,11 +22,6 @@ function generateId(length: number): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function ensureSetupToken() {
|
export async function ensureSetupToken() {
|
||||||
if (config.isManagedMode()) {
|
|
||||||
// LETS NOT WORRY ABOUT THE SERVER SECRET WHEN HYBRID
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if a server admin already exists
|
// Check if a server admin already exists
|
||||||
const [existingAdmin] = await db
|
const [existingAdmin] = await db
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import ProfileIcon from "@app/components/ProfileIcon";
|
|
||||||
import { verifySession } from "@app/lib/auth/verifySession";
|
import { verifySession } from "@app/lib/auth/verifySession";
|
||||||
import UserProvider from "@app/providers/UserProvider";
|
|
||||||
import { GetOrgResponse } from "@server/routers/org";
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
import { GetOrgUserResponse } from "@server/routers/user";
|
import { GetOrgUserResponse } from "@server/routers/user";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
@@ -10,7 +8,7 @@ import { redirect } from "next/navigation";
|
|||||||
import { cache } from "react";
|
import { cache } from "react";
|
||||||
import SetLastOrgCookie from "@app/components/SetLastOrgCookie";
|
import SetLastOrgCookie from "@app/components/SetLastOrgCookie";
|
||||||
import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
|
import SubscriptionStatusProvider from "@app/providers/SubscriptionStatusProvider";
|
||||||
import { GetOrgSubscriptionResponse } from "#private/routers/billing/getOrgSubscription";
|
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ import { InfoPopup } from "@/components/ui/info-popup";
|
|||||||
import {
|
import {
|
||||||
GetOrgSubscriptionResponse,
|
GetOrgSubscriptionResponse,
|
||||||
GetOrgUsageResponse
|
GetOrgUsageResponse
|
||||||
} from "#private/routers/billing";
|
} from "@server/routers/billing/types";
|
||||||
import { useTranslations } from "use-intl";
|
import { useTranslations } from "use-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { cache } from "react";
|
|||||||
import {
|
import {
|
||||||
GetOrgSubscriptionResponse,
|
GetOrgSubscriptionResponse,
|
||||||
GetOrgTierResponse
|
GetOrgTierResponse
|
||||||
} from "#private/routers/billing";
|
} from "@server/routers/billing/types";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode";
|
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import {
|
import {
|
||||||
QuickStartRemoteExitNodeResponse,
|
QuickStartRemoteExitNodeResponse,
|
||||||
PickRemoteExitNodeDefaultsResponse
|
PickRemoteExitNodeDefaultsResponse
|
||||||
} from "#private/routers/remoteExitNode";
|
} from "@server/routers/remoteExitNode/types";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
import { ListRemoteExitNodesResponse } from "#private/routers/remoteExitNode";
|
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
|
import ExitNodesTable, { RemoteExitNodeRow } from "./ExitNodesTable";
|
||||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export default function ResourceRules(props: {
|
|||||||
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const env = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const isMaxmindAvailable = env.env.server.maxmind_db_path && env.env.server.maxmind_db_path.length > 0;
|
const isMaxmindAvailable = env.server.maxmind_db_path && env.server.maxmind_db_path.length > 0;
|
||||||
|
|
||||||
const RuleAction = {
|
const RuleAction = {
|
||||||
ACCEPT: t('alwaysAllow'),
|
ACCEPT: t('alwaysAllow'),
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import {
|
|||||||
CreateSiteResponse,
|
CreateSiteResponse,
|
||||||
PickSiteDefaultsResponse
|
PickSiteDefaultsResponse
|
||||||
} from "@server/routers/site";
|
} from "@server/routers/site";
|
||||||
import { ListRemoteExitNodesResponse } from "#private/routers/remoteExitNode";
|
import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { useParams, useRouter } from "next/navigation";
|
import { useParams, useRouter } from "next/navigation";
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import { verifySession } from "@app/lib/auth/verifySession";
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { LoginFormIDP } from "@app/components/LoginForm";
|
import { LoginFormIDP } from "@app/components/LoginForm";
|
||||||
import { ListOrgIdpsResponse } from "#private/routers/orgIdp";
|
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import {
|
import {
|
||||||
GetLoginPageResponse,
|
|
||||||
LoadLoginPageResponse
|
LoadLoginPageResponse
|
||||||
} from "#private/routers/loginPage";
|
} from "@server/routers/loginPage/types";
|
||||||
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
import IdpLoginButtons from "@app/components/private/IdpLoginButtons";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -24,9 +23,9 @@ import {
|
|||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { GetSessionTransferTokenRenponse } from "#private/routers/auth/getSessionTransferToken";
|
import { GetSessionTransferTokenRenponse } from "@server/routers/auth/types";
|
||||||
import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken";
|
import ValidateSessionTransferToken from "@app/components/private/ValidateSessionTransferToken";
|
||||||
import { GetOrgTierResponse } from "#private/routers/billing";
|
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { AxiosResponse } from "axios";
|
|||||||
import { GetIdpResponse } from "@server/routers/idp";
|
import { GetIdpResponse } from "@server/routers/idp";
|
||||||
import { getTranslations } from "next-intl/server";
|
import { getTranslations } from "next-intl/server";
|
||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { LoadLoginPageResponse } from "#private/routers/loginPage";
|
import { LoadLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import AccessToken from "@app/components/AccessToken";
|
|||||||
import { pullEnv } from "@app/lib/pullEnv";
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
import { LoginFormIDP } from "@app/components/LoginForm";
|
import { LoginFormIDP } from "@app/components/LoginForm";
|
||||||
import { ListIdpsResponse } from "@server/routers/idp";
|
import { ListIdpsResponse } from "@server/routers/idp";
|
||||||
import { ListOrgIdpsResponse } from "#private/routers/orgIdp";
|
import { ListOrgIdpsResponse } from "@server/routers/orgIdp/types";
|
||||||
import AutoLoginHandler from "@app/components/AutoLoginHandler";
|
import AutoLoginHandler from "@app/components/AutoLoginHandler";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { headers } from "next/headers";
|
import { headers } from "next/headers";
|
||||||
import { GetLoginPageResponse } from "#private/routers/loginPage";
|
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
import { GetOrgTierResponse } from "#private/routers/billing";
|
import { GetOrgTierResponse } from "@server/routers/billing/types";
|
||||||
import { TierId } from "@server/lib/billing/tiers";
|
import { TierId } from "@server/lib/billing/tiers";
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ import { createApiClient, formatAxiosError } from "@/lib/api";
|
|||||||
import { useEnvContext } from "@/hooks/useEnvContext";
|
import { useEnvContext } from "@/hooks/useEnvContext";
|
||||||
import { toast } from "@/hooks/useToast";
|
import { toast } from "@/hooks/useToast";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain/listDomains";
|
import { ListDomainsResponse } from "@server/routers/domain/listDomains";
|
||||||
import { CheckDomainAvailabilityResponse } from "#private/routers/domain/checkDomainNamespaceAvailability";
|
import { CheckDomainAvailabilityResponse } from "@server/routers/domain/types";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ import {
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import CertificateStatus from "@app/components/private/CertificateStatus";
|
import CertificateStatus from "@app/components/private/CertificateStatus";
|
||||||
import { toUnicode } from 'punycode';
|
import { toUnicode } from "punycode";
|
||||||
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
type ResourceInfoBoxType = {};
|
type ResourceInfoBoxType = {};
|
||||||
|
|
||||||
export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
const { resource, authInfo } = useResourceContext();
|
const { resource, authInfo } = useResourceContext();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
|
||||||
@@ -28,7 +30,13 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{/* 4 cols because of the certs */}
|
{/* 4 cols because of the certs */}
|
||||||
<InfoSections cols={resource.http && build != "oss" ? 4 : 3}>
|
<InfoSections
|
||||||
|
cols={
|
||||||
|
resource.http && env.flags.generateOwnCertificates
|
||||||
|
? 4
|
||||||
|
: 3
|
||||||
|
}
|
||||||
|
>
|
||||||
{resource.http ? (
|
{resource.http ? (
|
||||||
<>
|
<>
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
@@ -37,9 +45,9 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
</InfoSectionTitle>
|
</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
{authInfo.password ||
|
{authInfo.password ||
|
||||||
authInfo.pincode ||
|
authInfo.pincode ||
|
||||||
authInfo.sso ||
|
authInfo.sso ||
|
||||||
authInfo.whitelist ? (
|
authInfo.whitelist ? (
|
||||||
<div className="flex items-start space-x-2 text-green-500">
|
<div className="flex items-start space-x-2 text-green-500">
|
||||||
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
<ShieldCheck className="w-4 h-4 mt-0.5" />
|
||||||
<span>{t("protected")}</span>
|
<span>{t("protected")}</span>
|
||||||
@@ -126,25 +134,28 @@ export default function ResourceInfoBox({ }: ResourceInfoBoxType) {
|
|||||||
{/* </InfoSectionContent> */}
|
{/* </InfoSectionContent> */}
|
||||||
{/* </InfoSection> */}
|
{/* </InfoSection> */}
|
||||||
{/* Certificate Status Column */}
|
{/* Certificate Status Column */}
|
||||||
{resource.http && resource.domainId && resource.fullDomain && build != "oss" && (
|
{resource.http &&
|
||||||
<InfoSection>
|
resource.domainId &&
|
||||||
<InfoSectionTitle>
|
resource.fullDomain &&
|
||||||
{t("certificateStatus", {
|
build != "oss" && (
|
||||||
defaultValue: "Certificate"
|
<InfoSection>
|
||||||
})}
|
<InfoSectionTitle>
|
||||||
</InfoSectionTitle>
|
{t("certificateStatus", {
|
||||||
<InfoSectionContent>
|
defaultValue: "Certificate"
|
||||||
<CertificateStatus
|
})}
|
||||||
orgId={resource.orgId}
|
</InfoSectionTitle>
|
||||||
domainId={resource.domainId}
|
<InfoSectionContent>
|
||||||
fullDomain={resource.fullDomain}
|
<CertificateStatus
|
||||||
autoFetch={true}
|
orgId={resource.orgId}
|
||||||
showLabel={false}
|
domainId={resource.domainId}
|
||||||
polling={true}
|
fullDomain={resource.fullDomain}
|
||||||
/>
|
autoFetch={true}
|
||||||
</InfoSectionContent>
|
showLabel={false}
|
||||||
</InfoSection>
|
polling={true}
|
||||||
)}
|
/>
|
||||||
|
</InfoSectionContent>
|
||||||
|
</InfoSection>
|
||||||
|
)}
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
<InfoSectionTitle>{t("visibility")}</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
@@ -30,7 +31,7 @@ import {
|
|||||||
SettingsSectionForm
|
SettingsSectionForm
|
||||||
} from "@app/components/Settings";
|
} from "@app/components/Settings";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { GetLoginPageResponse } from "#private/routers/loginPage";
|
import { GetLoginPageResponse } from "@server/routers/loginPage/types";
|
||||||
import { ListDomainsResponse } from "@server/routers/domain";
|
import { ListDomainsResponse } from "@server/routers/domain";
|
||||||
import { DomainRow } from "@app/components/DomainsTable";
|
import { DomainRow } from "@app/components/DomainsTable";
|
||||||
import { toUnicode } from "punycode";
|
import { toUnicode } from "punycode";
|
||||||
@@ -78,6 +79,7 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
|||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const subscription = useSubscriptionStatusContext();
|
const subscription = useSubscriptionStatusContext();
|
||||||
|
|
||||||
@@ -447,10 +449,21 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Certificate Status */}
|
{!form.watch(
|
||||||
{(build === "enterprise" ||
|
"authPageDomainId"
|
||||||
(build === "saas" &&
|
) && (
|
||||||
subscription?.subscribed)) &&
|
<div className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
"addDomainToEnableCustomAuthPages"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{env.flags
|
||||||
|
.generateOwnCertificates &&
|
||||||
|
(build === "enterprise" ||
|
||||||
|
(build === "saas" &&
|
||||||
|
subscription?.subscribed)) &&
|
||||||
loginPage?.domainId &&
|
loginPage?.domainId &&
|
||||||
loginPage?.fullDomain &&
|
loginPage?.fullDomain &&
|
||||||
!hasUnsavedChanges && (
|
!hasUnsavedChanges && (
|
||||||
@@ -469,16 +482,6 @@ const AuthPageSettings = forwardRef<AuthPageSettingsRef, AuthPageSettingsProps>(
|
|||||||
polling={true}
|
polling={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!form.watch(
|
|
||||||
"authPageDomainId"
|
|
||||||
) && (
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
{t(
|
|
||||||
"addDomainToEnableCustomAuthPages"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { TransferSessionResponse } from "#private/routers/auth/transferSession";
|
import { TransferSessionResponse } from "@server/routers/auth/types";
|
||||||
|
|
||||||
type ValidateSessionTransferTokenParams = {
|
type ValidateSessionTransferTokenParams = {
|
||||||
token: string;
|
token: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode";
|
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
type RemoteExitNodeContextType = {
|
type RemoteExitNodeContextType = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { GetOrgSubscriptionResponse } from "#private/routers/billing";
|
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||||
import { createContext } from "react";
|
import { createContext } from "react";
|
||||||
|
|
||||||
type SubscriptionStatusContextType = {
|
type SubscriptionStatusContextType = {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect } from "react";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { GetCertificateResponse } from "#private/routers/certificates";
|
import { GetCertificateResponse } from "@server/routers/certificates/types";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
|
|
||||||
|
|||||||
@@ -48,7 +48,11 @@ export function pullEnv(): Env {
|
|||||||
enableClients:
|
enableClients:
|
||||||
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
|
process.env.FLAGS_ENABLE_CLIENTS === "true" ? true : false,
|
||||||
hideSupporterKey:
|
hideSupporterKey:
|
||||||
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false
|
process.env.HIDE_SUPPORTER_KEY === "true" ? true : false,
|
||||||
|
generateOwnCertificates:
|
||||||
|
process.env.GENERATE_OWN_CERTIFICATES === "true"
|
||||||
|
? true
|
||||||
|
: false
|
||||||
},
|
},
|
||||||
|
|
||||||
branding: {
|
branding: {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export type Env = {
|
|||||||
disableBasicWireguardSites: boolean;
|
disableBasicWireguardSites: boolean;
|
||||||
enableClients: boolean;
|
enableClients: boolean;
|
||||||
hideSupporterKey: boolean;
|
hideSupporterKey: boolean;
|
||||||
|
generateOwnCertificates: boolean;
|
||||||
},
|
},
|
||||||
branding: {
|
branding: {
|
||||||
appName?: string;
|
appName?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext";
|
import RemoteExitNodeContext from "@app/contexts/remoteExitNodeContext";
|
||||||
import { GetRemoteExitNodeResponse } from "#private/routers/remoteExitNode";
|
import { GetRemoteExitNodeResponse } from "@server/routers/remoteExitNode/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import SubscriptionStatusContext from "@app/contexts/subscriptionStatusContext";
|
import SubscriptionStatusContext from "@app/contexts/subscriptionStatusContext";
|
||||||
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
|
import { getTierPriceSet, TierId } from "@server/lib/billing/tiers";
|
||||||
import { GetOrgSubscriptionResponse } from "#private/routers/billing";
|
import { GetOrgSubscriptionResponse } from "@server/routers/billing/types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user