mirror of
https://github.com/fosrl/pangolin.git
synced 2026-02-21 12:26:40 +00:00
Compare commits
12 Commits
1.0.0-beta
...
1.0.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5c507cc0ec | ||
|
|
55c0953fde | ||
|
|
844b12d363 | ||
|
|
f40d91ff9e | ||
|
|
f5e894e06a | ||
|
|
8fe479f809 | ||
|
|
9b9c343e2d | ||
|
|
cb1ccbe945 | ||
|
|
5de6028136 | ||
|
|
e226a5e86b | ||
|
|
f0ecfbb403 | ||
|
|
985418b9af |
@@ -9,7 +9,6 @@ server:
|
|||||||
internal_port: 3001
|
internal_port: 3001
|
||||||
next_port: 3002
|
next_port: 3002
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
secure_cookies: true
|
|
||||||
session_cookie_name: "p_session_token"
|
session_cookie_name: "p_session_token"
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
resource_session_request_param: "p_session_request"
|
resource_session_request_param: "p_session_request"
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ api:
|
|||||||
|
|
||||||
providers:
|
providers:
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/http"
|
endpoint: "http://pangolin:{{.INTERNAL_PORT}}/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
|
||||||
udp:
|
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/udp"
|
|
||||||
pollInterval: "5s"
|
|
||||||
tcp:
|
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp"
|
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ server:
|
|||||||
internal_port: 3001
|
internal_port: 3001
|
||||||
next_port: 3002
|
next_port: 3002
|
||||||
internal_hostname: "pangolin"
|
internal_hostname: "pangolin"
|
||||||
secure_cookies: true
|
|
||||||
session_cookie_name: "p_session_token"
|
session_cookie_name: "p_session_token"
|
||||||
resource_access_token_param: "p_token"
|
resource_access_token_param: "p_token"
|
||||||
resource_session_request_param: "p_session_request"
|
resource_session_request_param: "p_session_request"
|
||||||
@@ -40,7 +39,7 @@ rate_limits:
|
|||||||
{{if .EnableEmail}}
|
{{if .EnableEmail}}
|
||||||
email:
|
email:
|
||||||
smtp_host: "{{.EmailSMTPHost}}"
|
smtp_host: "{{.EmailSMTPHost}}"
|
||||||
smtp_port: "{{.EmailSMTPPort}}"
|
smtp_port: {{.EmailSMTPPort}}
|
||||||
smtp_user: "{{.EmailSMTPUser}}"
|
smtp_user: "{{.EmailSMTPUser}}"
|
||||||
smtp_pass: "{{.EmailSMTPPass}}"
|
smtp_pass: "{{.EmailSMTPPass}}"
|
||||||
no_reply: "{{.EmailNoReply}}"
|
no_reply: "{{.EmailNoReply}}"
|
||||||
|
|||||||
@@ -4,13 +4,7 @@ api:
|
|||||||
|
|
||||||
providers:
|
providers:
|
||||||
http:
|
http:
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/http"
|
endpoint: "http://pangolin:3001/api/v1/traefik-config"
|
||||||
pollInterval: "5s"
|
|
||||||
udp:
|
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/udp"
|
|
||||||
pollInterval: "5s"
|
|
||||||
tcp:
|
|
||||||
endpoint: "http://pangolin:3001/api/v1/traefik-config/tcp"
|
|
||||||
pollInterval: "5s"
|
pollInterval: "5s"
|
||||||
file:
|
file:
|
||||||
filename: "/etc/traefik/dynamic_config.yml"
|
filename: "/etc/traefik/dynamic_config.yml"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@fosrl/pangolin",
|
"name": "@fosrl/pangolin",
|
||||||
"version": "1.0.0-beta.9",
|
"version": "1.0.0-beta.10",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
"description": "Tunneled Reverse Proxy Management Server with Identity and Access Control and Dashboard UI",
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
import {
|
|
||||||
encodeBase32LowerCaseNoPadding,
|
|
||||||
encodeHexLowerCase,
|
|
||||||
} from "@oslojs/encoding";
|
|
||||||
import { sha256 } from "@oslojs/crypto/sha2";
|
|
||||||
import { Session, sessions, User, users } from "@server/db/schema";
|
|
||||||
import db from "@server/db";
|
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
import config from "@server/lib/config";
|
|
||||||
import type { RandomReader } from "@oslojs/crypto/random";
|
|
||||||
import { generateRandomString } from "@oslojs/crypto/random";
|
|
||||||
|
|
||||||
export const SESSION_COOKIE_NAME = config.getRawConfig().server.session_cookie_name;
|
|
||||||
export const SESSION_COOKIE_EXPIRES = 1000 * 60 * 60 * 24 * 30;
|
|
||||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
|
||||||
export const COOKIE_DOMAIN = "." + config.getBaseDomain();
|
|
||||||
|
|
||||||
export function generateSessionToken(): string {
|
|
||||||
const bytes = new Uint8Array(20);
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
const token = encodeBase32LowerCaseNoPadding(bytes);
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createSession(
|
|
||||||
token: string,
|
|
||||||
userId: string,
|
|
||||||
): Promise<Session> {
|
|
||||||
const sessionId = encodeHexLowerCase(
|
|
||||||
sha256(new TextEncoder().encode(token)),
|
|
||||||
);
|
|
||||||
const session: Session = {
|
|
||||||
sessionId: sessionId,
|
|
||||||
userId,
|
|
||||||
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
|
|
||||||
};
|
|
||||||
await db.insert(sessions).values(session);
|
|
||||||
return session;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function validateSessionToken(
|
|
||||||
token: string,
|
|
||||||
): Promise<SessionValidationResult> {
|
|
||||||
const sessionId = encodeHexLowerCase(
|
|
||||||
sha256(new TextEncoder().encode(token)),
|
|
||||||
);
|
|
||||||
const result = await db
|
|
||||||
.select({ user: users, session: sessions })
|
|
||||||
.from(sessions)
|
|
||||||
.innerJoin(users, eq(sessions.userId, users.userId))
|
|
||||||
.where(eq(sessions.sessionId, sessionId));
|
|
||||||
if (result.length < 1) {
|
|
||||||
return { session: null, user: null };
|
|
||||||
}
|
|
||||||
const { user, session } = result[0];
|
|
||||||
if (Date.now() >= session.expiresAt) {
|
|
||||||
await db
|
|
||||||
.delete(sessions)
|
|
||||||
.where(eq(sessions.sessionId, session.sessionId));
|
|
||||||
return { session: null, user: null };
|
|
||||||
}
|
|
||||||
if (Date.now() >= session.expiresAt - SESSION_COOKIE_EXPIRES / 2) {
|
|
||||||
session.expiresAt = new Date(
|
|
||||||
Date.now() + SESSION_COOKIE_EXPIRES,
|
|
||||||
).getTime();
|
|
||||||
await db
|
|
||||||
.update(sessions)
|
|
||||||
.set({
|
|
||||||
expiresAt: session.expiresAt,
|
|
||||||
})
|
|
||||||
.where(eq(sessions.sessionId, session.sessionId));
|
|
||||||
}
|
|
||||||
return { session, user };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invalidateSession(sessionId: string): Promise<void> {
|
|
||||||
await db.delete(sessions).where(eq(sessions.sessionId, sessionId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function invalidateAllSessions(userId: string): Promise<void> {
|
|
||||||
await db.delete(sessions).where(eq(sessions.userId, userId));
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serializeSessionCookie(token: string): string {
|
|
||||||
if (SECURE_COOKIES) {
|
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
} else {
|
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createBlankSessionTokenCookie(): string {
|
|
||||||
if (SECURE_COOKIES) {
|
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
} else {
|
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const random: RandomReader = {
|
|
||||||
read(bytes: Uint8Array): void {
|
|
||||||
crypto.getRandomValues(bytes);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export function generateId(length: number): string {
|
|
||||||
const alphabet = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
||||||
return generateRandomString(random, alphabet, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function generateIdFromEntropySize(size: number): string {
|
|
||||||
const buffer = crypto.getRandomValues(new Uint8Array(size));
|
|
||||||
return encodeBase32LowerCaseNoPadding(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SessionValidationResult =
|
|
||||||
| { session: Session; user: User }
|
|
||||||
| { session: null; user: null };
|
|
||||||
@@ -24,7 +24,6 @@ export const SESSION_COOKIE_EXPIRES =
|
|||||||
60 *
|
60 *
|
||||||
60 *
|
60 *
|
||||||
config.getRawConfig().server.dashboard_session_length_hours;
|
config.getRawConfig().server.dashboard_session_length_hours;
|
||||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
|
||||||
export const COOKIE_DOMAIN =
|
export const COOKIE_DOMAIN =
|
||||||
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
"." + new URL(config.getRawConfig().app.dashboard_url).hostname;
|
||||||
|
|
||||||
@@ -108,12 +107,7 @@ export function serializeSessionCookie(
|
|||||||
isSecure: boolean
|
isSecure: boolean
|
||||||
): string {
|
): string {
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
logger.debug("Setting cookie for secure origin");
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
if (SECURE_COOKIES) {
|
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
} else {
|
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
return `${SESSION_COOKIE_NAME}=${token}; HttpOnly; SameSite=Lax; Max-Age=${SESSION_COOKIE_EXPIRES}; Path=/;`;
|
||||||
}
|
}
|
||||||
@@ -121,11 +115,7 @@ export function serializeSessionCookie(
|
|||||||
|
|
||||||
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
export function createBlankSessionTokenCookie(isSecure: boolean): string {
|
||||||
if (isSecure) {
|
if (isSecure) {
|
||||||
if (SECURE_COOKIES) {
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
} else {
|
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${COOKIE_DOMAIN}`;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
return `${SESSION_COOKIE_NAME}=; HttpOnly; SameSite=Lax; Max-Age=0; Path=/;`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ export const SESSION_COOKIE_NAME =
|
|||||||
config.getRawConfig().server.session_cookie_name;
|
config.getRawConfig().server.session_cookie_name;
|
||||||
export const SESSION_COOKIE_EXPIRES =
|
export const SESSION_COOKIE_EXPIRES =
|
||||||
1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours;
|
1000 * 60 * 60 * config.getRawConfig().server.resource_session_length_hours;
|
||||||
export const SECURE_COOKIES = config.getRawConfig().server.secure_cookies;
|
|
||||||
|
|
||||||
export async function createResourceSession(opts: {
|
export async function createResourceSession(opts: {
|
||||||
token: string;
|
token: string;
|
||||||
@@ -170,7 +169,7 @@ export function serializeResourceSessionCookie(
|
|||||||
token: string,
|
token: string,
|
||||||
isHttp: boolean = false
|
isHttp: boolean = false
|
||||||
): string {
|
): string {
|
||||||
if (SECURE_COOKIES && !isHttp) {
|
if (!isHttp) {
|
||||||
return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
|
return `${cookieName}_s=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Secure; Domain=${"." + domain}`;
|
||||||
} else {
|
} else {
|
||||||
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
|
return `${cookieName}=${token}; HttpOnly; SameSite=Strict; Max-Age=${SESSION_COOKIE_EXPIRES / 1000}; Path=/; Domain=${"." + domain}`;
|
||||||
@@ -179,9 +178,10 @@ export function serializeResourceSessionCookie(
|
|||||||
|
|
||||||
export function createBlankResourceSessionTokenCookie(
|
export function createBlankResourceSessionTokenCookie(
|
||||||
cookieName: string,
|
cookieName: string,
|
||||||
domain: string
|
domain: string,
|
||||||
|
isHttp: boolean = false
|
||||||
): string {
|
): string {
|
||||||
if (SECURE_COOKIES) {
|
if (!isHttp) {
|
||||||
return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
|
return `${cookieName}_s=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Secure; Domain=${"." + domain}`;
|
||||||
} else {
|
} else {
|
||||||
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;
|
return `${cookieName}=; HttpOnly; SameSite=Strict; Max-Age=0; Path=/; Domain=${"." + domain}`;
|
||||||
|
|||||||
@@ -60,7 +60,6 @@ const configSchema = z.object({
|
|||||||
.transform(stoi)
|
.transform(stoi)
|
||||||
.pipe(portSchema),
|
.pipe(portSchema),
|
||||||
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
internal_hostname: z.string().transform((url) => url.toLowerCase()),
|
||||||
secure_cookies: z.boolean(),
|
|
||||||
session_cookie_name: z.string(),
|
session_cookie_name: z.string(),
|
||||||
resource_access_token_param: z.string(),
|
resource_access_token_param: z.string(),
|
||||||
resource_session_request_param: z.string(),
|
resource_session_request_param: z.string(),
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
serializeResourceSessionCookie,
|
serializeResourceSessionCookie,
|
||||||
validateResourceSessionToken
|
validateResourceSessionToken
|
||||||
} from "@server/auth/sessions/resource";
|
} from "@server/auth/sessions/resource";
|
||||||
import { generateSessionToken } from "@server/auth";
|
import { generateSessionToken, SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
|
||||||
import { SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/app";
|
|
||||||
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
import { SESSION_COOKIE_EXPIRES as RESOURCE_SESSION_COOKIE_EXPIRES } from "@server/auth/sessions/resource";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { response } from "@server/lib";
|
import { response } from "@server/lib";
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ import {
|
|||||||
import { Resource, roleResources, userResources } from "@server/db/schema";
|
import { Resource, roleResources, userResources } from "@server/db/schema";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToken";
|
||||||
import { generateSessionToken } from "@server/auth";
|
|
||||||
import NodeCache from "node-cache";
|
import NodeCache from "node-cache";
|
||||||
|
import { generateSessionToken } from "@server/auth/sessions/app";
|
||||||
|
|
||||||
// We'll see if this speeds anything up
|
// We'll see if this speeds anything up
|
||||||
const cache = new NodeCache({
|
const cache = new NodeCache({
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Router } from "express";
|
import { Router } from "express";
|
||||||
import * as gerbil from "@server/routers/gerbil";
|
import * as gerbil from "@server/routers/gerbil";
|
||||||
import * as traefik from "@server/routers/traefik";
|
import * as traefik from "@server/routers/traefik";
|
||||||
|
import * as resource from "./resource";
|
||||||
|
import * as badger from "./badger";
|
||||||
import * as auth from "@server/routers/auth";
|
import * as auth from "@server/routers/auth";
|
||||||
import HttpCode from "@server/types/HttpCode";
|
import HttpCode from "@server/types/HttpCode";
|
||||||
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
|
import { verifyResourceAccess, verifySessionUserMiddleware } from "@server/middlewares";
|
||||||
import { getExchangeToken } from "./resource/getExchangeToken";
|
|
||||||
import { verifyResourceSession } from "./badger";
|
|
||||||
import { exchangeSession } from "./badger/exchangeSession";
|
|
||||||
|
|
||||||
// Root routes
|
// Root routes
|
||||||
const internalRouter = Router();
|
const internalRouter = Router();
|
||||||
@@ -26,7 +25,7 @@ internalRouter.post(
|
|||||||
`/resource/:resourceId/get-exchange-token`,
|
`/resource/:resourceId/get-exchange-token`,
|
||||||
verifySessionUserMiddleware,
|
verifySessionUserMiddleware,
|
||||||
verifyResourceAccess,
|
verifyResourceAccess,
|
||||||
getExchangeToken
|
resource.getExchangeToken
|
||||||
);
|
);
|
||||||
|
|
||||||
// Gerbil routes
|
// Gerbil routes
|
||||||
@@ -40,7 +39,7 @@ gerbilRouter.post("/receive-bandwidth", gerbil.receiveBandwidth);
|
|||||||
const badgerRouter = Router();
|
const badgerRouter = Router();
|
||||||
internalRouter.use("/badger", badgerRouter);
|
internalRouter.use("/badger", badgerRouter);
|
||||||
|
|
||||||
badgerRouter.post("/verify-session", verifyResourceSession);
|
badgerRouter.post("/verify-session", badger.verifyResourceSession);
|
||||||
badgerRouter.post("/exchange-session", exchangeSession);
|
badgerRouter.post("/exchange-session", badger.exchangeSession);
|
||||||
|
|
||||||
export default internalRouter;
|
export default internalRouter;
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ export const handleRegisterMessage: MessageHandler = async (context) => {
|
|||||||
eq(targets.enabled, true)
|
eq(targets.enabled, true)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
.where(eq(resources.siteId, siteId))
|
||||||
.groupBy(resources.resourceId);
|
.groupBy(resources.resourceId);
|
||||||
|
|
||||||
let tcpTargets: string[] = [];
|
let tcpTargets: string[] = [];
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from "path";
|
|||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { versionMigrations } from "@server/db/schema";
|
import { versionMigrations } from "@server/db/schema";
|
||||||
import { desc } from "drizzle-orm";
|
import { desc } from "drizzle-orm";
|
||||||
import { __DIRNAME, APP_PATH } from "@server/lib/consts";
|
import { __DIRNAME } from "@server/lib/consts";
|
||||||
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
import { loadAppVersion } from "@server/lib/loadAppVersion";
|
||||||
import m1 from "./scripts/1.0.0-beta1";
|
import m1 from "./scripts/1.0.0-beta1";
|
||||||
import m2 from "./scripts/1.0.0-beta2";
|
import m2 from "./scripts/1.0.0-beta2";
|
||||||
@@ -12,6 +12,7 @@ import m3 from "./scripts/1.0.0-beta3";
|
|||||||
import m4 from "./scripts/1.0.0-beta5";
|
import m4 from "./scripts/1.0.0-beta5";
|
||||||
import m5 from "./scripts/1.0.0-beta6";
|
import m5 from "./scripts/1.0.0-beta6";
|
||||||
import m6 from "./scripts/1.0.0-beta9";
|
import m6 from "./scripts/1.0.0-beta9";
|
||||||
|
import m7 from "./scripts/1.0.0-beta10";
|
||||||
|
|
||||||
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
// THIS CANNOT IMPORT ANYTHING FROM THE SERVER
|
||||||
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
// EXCEPT FOR THE DATABASE AND THE SCHEMA
|
||||||
@@ -23,7 +24,8 @@ const migrations = [
|
|||||||
{ version: "1.0.0-beta.3", run: m3 },
|
{ version: "1.0.0-beta.3", run: m3 },
|
||||||
{ version: "1.0.0-beta.5", run: m4 },
|
{ version: "1.0.0-beta.5", run: m4 },
|
||||||
{ version: "1.0.0-beta.6", run: m5 },
|
{ version: "1.0.0-beta.6", run: m5 },
|
||||||
{ version: "1.0.0-beta.9", run: m6 }
|
{ version: "1.0.0-beta.9", run: m6 },
|
||||||
|
{ version: "1.0.0-beta.10", run: m7 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
|
|||||||
45
server/setup/scripts/1.0.0-beta10.ts
Normal file
45
server/setup/scripts/1.0.0-beta10.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log("Running setup script 1.0.0-beta.10...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Determine which config file exists
|
||||||
|
const filePaths = [configFilePath1, configFilePath2];
|
||||||
|
let filePath = "";
|
||||||
|
for (const path of filePaths) {
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
filePath = path;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filePath) {
|
||||||
|
throw new Error(
|
||||||
|
`No config file found (expected config.yml or config.yaml).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read and parse the YAML file
|
||||||
|
let rawConfig: any;
|
||||||
|
const fileContents = fs.readFileSync(filePath, "utf8");
|
||||||
|
rawConfig = yaml.load(fileContents);
|
||||||
|
|
||||||
|
delete rawConfig.server.secure_cookies;
|
||||||
|
|
||||||
|
// Write the updated YAML back to the file
|
||||||
|
const updatedYaml = yaml.dump(rawConfig);
|
||||||
|
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||||
|
|
||||||
|
console.log(`Removed deprecated config option: secure_cookies.`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
`Was unable to remove deprecated config option: secure_cookies. Error: ${e}`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ import {
|
|||||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { SquareArrowOutUpRight } from "lucide-react";
|
import { SquareArrowOutUpRight } from "lucide-react";
|
||||||
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
|
||||||
const createResourceFormSchema = z
|
const createResourceFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -129,6 +130,10 @@ export default function CreateResourceForm({
|
|||||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||||
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
|
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
|
||||||
|
|
||||||
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
|
|
||||||
|
const [resourceId, setResourceId] = useState<number | null>(null);
|
||||||
|
|
||||||
const form = useForm<CreateResourceFormValues>({
|
const form = useForm<CreateResourceFormValues>({
|
||||||
resolver: zodResolver(createResourceFormSchema),
|
resolver: zodResolver(createResourceFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -186,11 +191,21 @@ export default function CreateResourceForm({
|
|||||||
|
|
||||||
if (res && res.status === 201) {
|
if (res && res.status === 201) {
|
||||||
const id = res.data.data.resourceId;
|
const id = res.data.data.resourceId;
|
||||||
// navigate to the resource page
|
setResourceId(id);
|
||||||
router.push(`/${orgId}/settings/resources/${id}`);
|
|
||||||
|
if (data.http) {
|
||||||
|
goToResource();
|
||||||
|
} else {
|
||||||
|
setShowSnippets(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function goToResource() {
|
||||||
|
// navigate to the resource page
|
||||||
|
router.push(`/${orgId}/settings/resources/${resourceId}`);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Credenza
|
<Credenza
|
||||||
@@ -211,284 +226,358 @@ export default function CreateResourceForm({
|
|||||||
</CredenzaDescription>
|
</CredenzaDescription>
|
||||||
</CredenzaHeader>
|
</CredenzaHeader>
|
||||||
<CredenzaBody>
|
<CredenzaBody>
|
||||||
<Form {...form}>
|
{!showSnippets && (
|
||||||
<form
|
<Form {...form}>
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
<form
|
||||||
className="space-y-4"
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
id="create-resource-form"
|
className="space-y-4"
|
||||||
>
|
id="create-resource-form"
|
||||||
<FormField
|
>
|
||||||
control={form.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Name</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Your name"
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
This is the name that will be
|
|
||||||
displayed for this resource.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{!env.flags.allowRawResources || (
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="http"
|
name="name"
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
|
||||||
<div className="space-y-0.5">
|
|
||||||
<FormLabel className="text-base">
|
|
||||||
HTTP Resource
|
|
||||||
</FormLabel>
|
|
||||||
<FormDescription>
|
|
||||||
Toggle if this is an
|
|
||||||
HTTP resource or a raw
|
|
||||||
TCP/UDP resource
|
|
||||||
</FormDescription>
|
|
||||||
</div>
|
|
||||||
<FormControl>
|
|
||||||
<Switch
|
|
||||||
checked={field.value}
|
|
||||||
onCheckedChange={
|
|
||||||
field.onChange
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{form.watch("http") && (
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="subdomain"
|
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>Subdomain</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<CustomDomainInput
|
<Input
|
||||||
value={
|
placeholder="Your name"
|
||||||
field.value ?? ""
|
{...field}
|
||||||
}
|
|
||||||
domainSuffix={
|
|
||||||
domainSuffix
|
|
||||||
}
|
|
||||||
placeholder="Enter subdomain"
|
|
||||||
onChange={(value) =>
|
|
||||||
form.setValue(
|
|
||||||
"subdomain",
|
|
||||||
value
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This is the fully qualified
|
This is the name that will
|
||||||
domain name that will be
|
be displayed for this
|
||||||
used to access the resource.
|
resource.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
{!form.watch("http") && (
|
{!env.flags.allowRawResources || (
|
||||||
<Link
|
|
||||||
className="text-sm text-primary flex items-center gap-1"
|
|
||||||
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
Learn how to configure TCP/UDP resources
|
|
||||||
</span>
|
|
||||||
<SquareArrowOutUpRight size={14} />
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!form.watch("http") && (
|
|
||||||
<>
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="protocol"
|
name="http"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||||
<FormLabel>
|
<div className="space-y-0.5">
|
||||||
Protocol
|
<FormLabel className="text-base">
|
||||||
</FormLabel>
|
HTTP Resource
|
||||||
<Select
|
</FormLabel>
|
||||||
value={field.value}
|
<FormDescription>
|
||||||
onValueChange={
|
Toggle if this is an
|
||||||
field.onChange
|
HTTP resource or a
|
||||||
}
|
raw TCP/UDP resource
|
||||||
>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<SelectTrigger>
|
<FormControl>
|
||||||
<SelectValue placeholder="Select a protocol" />
|
<Switch
|
||||||
</SelectTrigger>
|
checked={
|
||||||
</FormControl>
|
field.value
|
||||||
<SelectContent>
|
}
|
||||||
<SelectItem value="tcp">
|
onCheckedChange={
|
||||||
TCP
|
field.onChange
|
||||||
</SelectItem>
|
}
|
||||||
<SelectItem value="udp">
|
/>
|
||||||
UDP
|
</FormControl>
|
||||||
</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<FormDescription>
|
|
||||||
The protocol to use for
|
|
||||||
the resource
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{form.watch("http") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="proxyPort"
|
name="subdomain"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
Port Number
|
Subdomain
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<CustomDomainInput
|
||||||
type="number"
|
|
||||||
placeholder="Enter port number"
|
|
||||||
value={
|
value={
|
||||||
field.value ??
|
field.value ??
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
onChange={(e) =>
|
domainSuffix={
|
||||||
field.onChange(
|
domainSuffix
|
||||||
e.target
|
}
|
||||||
.value
|
placeholder="Enter subdomain"
|
||||||
? parseInt(
|
onChange={(value) =>
|
||||||
e
|
form.setValue(
|
||||||
.target
|
"subdomain",
|
||||||
.value
|
value
|
||||||
)
|
|
||||||
: null
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
The port number to proxy
|
This is the fully
|
||||||
requests to (required
|
qualified domain name
|
||||||
for non-HTTP resources)
|
that will be used to
|
||||||
|
access the resource.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="siteId"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem className="flex flex-col">
|
|
||||||
<FormLabel>Site</FormLabel>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<FormControl>
|
|
||||||
<Button
|
|
||||||
variant="outline"
|
|
||||||
role="combobox"
|
|
||||||
className={cn(
|
|
||||||
"justify-between",
|
|
||||||
!field.value &&
|
|
||||||
"text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{field.value
|
|
||||||
? sites.find(
|
|
||||||
(site) =>
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
)?.name
|
|
||||||
: "Select site"}
|
|
||||||
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
|
||||||
</Button>
|
|
||||||
</FormControl>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent className="p-0">
|
|
||||||
<Command>
|
|
||||||
<CommandInput placeholder="Search site..." />
|
|
||||||
<CommandList>
|
|
||||||
<CommandEmpty>
|
|
||||||
No site found.
|
|
||||||
</CommandEmpty>
|
|
||||||
<CommandGroup>
|
|
||||||
{sites.map(
|
|
||||||
(site) => (
|
|
||||||
<CommandItem
|
|
||||||
value={
|
|
||||||
site.niceId
|
|
||||||
}
|
|
||||||
key={
|
|
||||||
site.siteId
|
|
||||||
}
|
|
||||||
onSelect={() => {
|
|
||||||
form.setValue(
|
|
||||||
"siteId",
|
|
||||||
site.siteId
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<CheckIcon
|
|
||||||
className={cn(
|
|
||||||
"mr-2 h-4 w-4",
|
|
||||||
site.siteId ===
|
|
||||||
field.value
|
|
||||||
? "opacity-100"
|
|
||||||
: "opacity-0"
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
{
|
|
||||||
site.name
|
|
||||||
}
|
|
||||||
</CommandItem>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</CommandGroup>
|
|
||||||
</CommandList>
|
|
||||||
</Command>
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<FormDescription>
|
|
||||||
This is the site that will be
|
|
||||||
used in the dashboard.
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
</form>
|
{!form.watch("http") && (
|
||||||
</Form>
|
<Link
|
||||||
|
className="text-sm text-primary flex items-center gap-1"
|
||||||
|
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Learn how to configure TCP/UDP
|
||||||
|
resources
|
||||||
|
</span>
|
||||||
|
<SquareArrowOutUpRight size={14} />
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!form.watch("http") && (
|
||||||
|
<>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="protocol"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Protocol
|
||||||
|
</FormLabel>
|
||||||
|
<Select
|
||||||
|
value={field.value}
|
||||||
|
onValueChange={
|
||||||
|
field.onChange
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControl>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Select a protocol" />
|
||||||
|
</SelectTrigger>
|
||||||
|
</FormControl>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="tcp">
|
||||||
|
TCP
|
||||||
|
</SelectItem>
|
||||||
|
<SelectItem value="udp">
|
||||||
|
UDP
|
||||||
|
</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<FormDescription>
|
||||||
|
The protocol to use
|
||||||
|
for the resource
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="proxyPort"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>
|
||||||
|
Port Number
|
||||||
|
</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
placeholder="Enter port number"
|
||||||
|
value={
|
||||||
|
field.value ??
|
||||||
|
""
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
field.onChange(
|
||||||
|
e.target
|
||||||
|
.value
|
||||||
|
? parseInt(
|
||||||
|
e
|
||||||
|
.target
|
||||||
|
.value
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
The port number to
|
||||||
|
proxy requests to
|
||||||
|
(required for
|
||||||
|
non-HTTP resources)
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="siteId"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem className="flex flex-col">
|
||||||
|
<FormLabel>Site</FormLabel>
|
||||||
|
<Popover>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<FormControl>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
role="combobox"
|
||||||
|
className={cn(
|
||||||
|
"justify-between",
|
||||||
|
!field.value &&
|
||||||
|
"text-muted-foreground"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{field.value
|
||||||
|
? sites.find(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) =>
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
)?.name
|
||||||
|
: "Select site"}
|
||||||
|
<CaretSortIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||||
|
</Button>
|
||||||
|
</FormControl>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="p-0">
|
||||||
|
<Command>
|
||||||
|
<CommandInput placeholder="Search site..." />
|
||||||
|
<CommandList>
|
||||||
|
<CommandEmpty>
|
||||||
|
No site
|
||||||
|
found.
|
||||||
|
</CommandEmpty>
|
||||||
|
<CommandGroup>
|
||||||
|
{sites.map(
|
||||||
|
(
|
||||||
|
site
|
||||||
|
) => (
|
||||||
|
<CommandItem
|
||||||
|
value={
|
||||||
|
site.niceId
|
||||||
|
}
|
||||||
|
key={
|
||||||
|
site.siteId
|
||||||
|
}
|
||||||
|
onSelect={() => {
|
||||||
|
form.setValue(
|
||||||
|
"siteId",
|
||||||
|
site.siteId
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CheckIcon
|
||||||
|
className={cn(
|
||||||
|
"mr-2 h-4 w-4",
|
||||||
|
site.siteId ===
|
||||||
|
field.value
|
||||||
|
? "opacity-100"
|
||||||
|
: "opacity-0"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{
|
||||||
|
site.name
|
||||||
|
}
|
||||||
|
</CommandItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</CommandGroup>
|
||||||
|
</CommandList>
|
||||||
|
</Command>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
<FormDescription>
|
||||||
|
This is the site that will
|
||||||
|
be used in the dashboard.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{showSnippets && (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
|
||||||
|
1
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
Traefik: Add Entrypoints
|
||||||
|
</h3>
|
||||||
|
<CopyTextBox
|
||||||
|
text={`entryPoints:
|
||||||
|
${form.getValues("protocol")}-${form.getValues("proxyPort")}:
|
||||||
|
address: ":${form.getValues("proxyPort")}/${form.getValues("protocol")}"`}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4 mb-6 last:mb-0">
|
||||||
|
<div className="flex-shrink-0 w-8 h-8 bg-muted text-primary-foreground rounded-full flex items-center justify-center font-bold">
|
||||||
|
2
|
||||||
|
</div>
|
||||||
|
<div className="flex-grow">
|
||||||
|
<h3 className="text-lg font-semibold mb-3">
|
||||||
|
Gerbil: Expose Ports in Docker
|
||||||
|
Compose
|
||||||
|
</h3>
|
||||||
|
<CopyTextBox
|
||||||
|
text={`ports:
|
||||||
|
- ${form.getValues("proxyPort")}:${form.getValues("proxyPort")}${form.getValues("protocol") === "tcp" ? "" : "/" + form.getValues("protocol")}`}
|
||||||
|
wrapText={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="text-sm text-primary flex items-center gap-1"
|
||||||
|
href="https://docs.fossorial.io/Getting%20Started/tcp-udp"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
Make sure to follow the full guide
|
||||||
|
</span>
|
||||||
|
<SquareArrowOutUpRight size={14} />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
<Button
|
{!showSnippets && <Button
|
||||||
type="submit"
|
type="submit"
|
||||||
form="create-resource-form"
|
form="create-resource-form"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
>
|
>
|
||||||
Create Resource
|
Create Resource
|
||||||
</Button>
|
</Button>}
|
||||||
|
|
||||||
|
{showSnippets && <Button
|
||||||
|
loading={loading}
|
||||||
|
onClick={() => goToResource()}
|
||||||
|
>
|
||||||
|
Go to Resource
|
||||||
|
</Button>}
|
||||||
|
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">Close</Button>
|
<Button variant="outline">Close</Button>
|
||||||
</CredenzaClose>
|
</CredenzaClose>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ export default function CustomDomainInput({
|
|||||||
className="rounded-r-none flex-grow"
|
className="rounded-r-none flex-grow"
|
||||||
/>
|
/>
|
||||||
<div className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-muted-foreground">
|
<div className="inline-flex items-center px-3 rounded-r-md border border-l-0 border-input bg-muted text-muted-foreground">
|
||||||
<span className="text-sm">{domainSuffix}</span>
|
<span className="text-sm">.{domainSuffix}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ export default function ReverseProxyTargets(props: {
|
|||||||
const addTargetForm = useForm({
|
const addTargetForm = useForm({
|
||||||
resolver: zodResolver(addTargetSchema),
|
resolver: zodResolver(addTargetSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
ip: "localhost",
|
ip: "",
|
||||||
method: resource.http ? "http" : null,
|
method: resource.http ? "http" : null,
|
||||||
port: resource.http ? 80 : resource.proxyPort || 1234
|
port: resource.http ? 80 : resource.proxyPort || 1234
|
||||||
// protocol: "TCP",
|
// protocol: "TCP",
|
||||||
|
|||||||
Reference in New Issue
Block a user