mirror of
https://github.com/fosrl/pangolin.git
synced 2025-05-14 06:10:39 +01:00
Merge branch 'dev' into wildcard-resource
This commit is contained in:
commit
fd806f5061
20 changed files with 555 additions and 192 deletions
9
.github/workflows/cicd.yml
vendored
9
.github/workflows/cicd.yml
vendored
|
@ -35,13 +35,8 @@ jobs:
|
||||||
- name: Update version in package.json
|
- name: Update version in package.json
|
||||||
run: |
|
run: |
|
||||||
TAG=${{ env.TAG }}
|
TAG=${{ env.TAG }}
|
||||||
if [ -f package.json ]; then
|
sed -i "s/export const APP_VERSION = \".*\";/export const APP_VERSION = \"$TAG\";/" server/lib/consts.ts
|
||||||
jq --arg version "$TAG" '.version = $version' package.json > package.tmp.json && mv package.tmp.json package.json
|
cat server/lib/
|
||||||
echo "Updated package.json with version $TAG"
|
|
||||||
else
|
|
||||||
echo "package.json not found"
|
|
||||||
fi
|
|
||||||
cat package.json
|
|
||||||
|
|
||||||
- name: Pull latest Gerbil version
|
- name: Pull latest Gerbil version
|
||||||
id: get-gerbil-tag
|
id: get-gerbil-tag
|
||||||
|
|
|
@ -37,7 +37,7 @@ services:
|
||||||
- 80:80 # Port for traefik because of the network_mode
|
- 80:80 # Port for traefik because of the network_mode
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.1
|
image: traefik:v3.3.3
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
network_mode: service:gerbil # Ports appear on the gerbil service
|
network_mode: service:gerbil # Ports appear on the gerbil service
|
||||||
|
@ -49,3 +49,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
name: pangolin
|
|
@ -36,7 +36,7 @@ services:
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
||||||
traefik:
|
traefik:
|
||||||
image: traefik:v3.1
|
image: traefik:v3.3.3
|
||||||
container_name: traefik
|
container_name: traefik
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
{{if .InstallGerbil}}
|
{{if .InstallGerbil}}
|
||||||
|
@ -55,3 +55,8 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
- ./config/traefik:/etc/traefik:ro # Volume to store the Traefik configuration
|
||||||
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
- ./config/letsencrypt:/letsencrypt # Volume to store the Let's Encrypt certificates
|
||||||
|
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
driver: bridge
|
||||||
|
name: pangolin
|
|
@ -53,7 +53,8 @@ export const resources = sqliteTable("resources", {
|
||||||
proxyPort: integer("proxyPort"),
|
proxyPort: integer("proxyPort"),
|
||||||
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
emailWhitelistEnabled: integer("emailWhitelistEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(false)
|
.default(false),
|
||||||
|
isBaseDomain: integer("isBaseDomain", { mode: "boolean" })
|
||||||
});
|
});
|
||||||
|
|
||||||
export const targets = sqliteTable("targets", {
|
export const targets = sqliteTable("targets", {
|
||||||
|
|
|
@ -151,7 +151,8 @@ const configSchema = z.object({
|
||||||
require_email_verification: z.boolean().optional(),
|
require_email_verification: z.boolean().optional(),
|
||||||
disable_signup_without_invite: z.boolean().optional(),
|
disable_signup_without_invite: z.boolean().optional(),
|
||||||
disable_user_create_org: z.boolean().optional(),
|
disable_user_create_org: z.boolean().optional(),
|
||||||
allow_raw_resources: z.boolean().optional()
|
allow_raw_resources: z.boolean().optional(),
|
||||||
|
allow_base_domain_resources: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.optional()
|
.optional()
|
||||||
});
|
});
|
||||||
|
@ -251,9 +252,9 @@ export class Config {
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
|
process.env.FLAGS_ALLOW_RAW_RESOURCES = parsedConfig.data.flags
|
||||||
?.allow_raw_resources
|
?.allow_raw_resources
|
||||||
? "true"
|
? "true"
|
||||||
: "false";
|
: "false";
|
||||||
process.env.SESSION_COOKIE_NAME =
|
process.env.SESSION_COOKIE_NAME =
|
||||||
parsedConfig.data.server.session_cookie_name;
|
parsedConfig.data.server.session_cookie_name;
|
||||||
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
process.env.EMAIL_ENABLED = parsedConfig.data.email ? "true" : "false";
|
||||||
|
@ -269,6 +270,11 @@ export class Config {
|
||||||
parsedConfig.data.server.resource_access_token_param;
|
parsedConfig.data.server.resource_access_token_param;
|
||||||
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
process.env.RESOURCE_SESSION_REQUEST_PARAM =
|
||||||
parsedConfig.data.server.resource_session_request_param;
|
parsedConfig.data.server.resource_session_request_param;
|
||||||
|
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES = parsedConfig.data.flags
|
||||||
|
?.allow_base_domain_resources
|
||||||
|
? "true"
|
||||||
|
: "false";
|
||||||
|
process.env.DASHBOARD_URL = parsedConfig.data.app.dashboard_url;
|
||||||
|
|
||||||
this.rawConfig = parsedConfig.data;
|
this.rawConfig = parsedConfig.data;
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,8 @@ const createResourceSchema = z
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
proxyPort: z.number().optional()
|
proxyPort: z.number().optional(),
|
||||||
|
isBaseDomain: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -55,7 +56,7 @@ const createResourceSchema = z
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.http) {
|
if (data.http && !data.isBaseDomain) {
|
||||||
return subdomainSchema.safeParse(data.subdomain).success;
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -75,18 +76,31 @@ const createResourceSchema = z
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Cannot update proxyPort"
|
message: "Proxy port cannot be set"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
// .refine(
|
||||||
|
// (data) => {
|
||||||
|
// if (data.proxyPort === 443 || data.proxyPort === 80) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// message: "Port 80 and 443 are reserved for http and https resources"
|
||||||
|
// }
|
||||||
|
// )
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.proxyPort === 443 || data.proxyPort === 80) {
|
if (!config.getRawConfig().flags?.allow_base_domain_resources) {
|
||||||
return false;
|
if (data.isBaseDomain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Port 80 and 443 are reserved for http and https resources"
|
message: "Base domain resources are not allowed"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -108,7 +122,7 @@ export async function createResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let { name, subdomain, protocol, proxyPort, http } = parsedBody.data;
|
let { name, subdomain, protocol, proxyPort, http, isBaseDomain } = parsedBody.data;
|
||||||
|
|
||||||
// Validate request params
|
// Validate request params
|
||||||
const parsedParams = createResourceParamsSchema.safeParse(req.params);
|
const parsedParams = createResourceParamsSchema.safeParse(req.params);
|
||||||
|
@ -145,7 +159,13 @@ export async function createResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullDomain = `${subdomain}.${org[0].domain}`;
|
let fullDomain = "";
|
||||||
|
if (isBaseDomain) {
|
||||||
|
fullDomain = org[0].domain;
|
||||||
|
} else {
|
||||||
|
fullDomain = `${subdomain}.${org[0].domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
// if http is false check to see if there is already a resource with the same port and protocol
|
// if http is false check to see if there is already a resource with the same port and protocol
|
||||||
if (!http) {
|
if (!http) {
|
||||||
const existingResource = await db
|
const existingResource = await db
|
||||||
|
@ -195,7 +215,8 @@ export async function createResource(
|
||||||
http,
|
http,
|
||||||
protocol,
|
protocol,
|
||||||
proxyPort,
|
proxyPort,
|
||||||
ssl: true
|
ssl: true,
|
||||||
|
isBaseDomain
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,8 @@ const updateResourceBodySchema = z
|
||||||
sso: z.boolean().optional(),
|
sso: z.boolean().optional(),
|
||||||
blockAccess: z.boolean().optional(),
|
blockAccess: z.boolean().optional(),
|
||||||
proxyPort: z.number().int().min(1).max(65535).optional(),
|
proxyPort: z.number().int().min(1).max(65535).optional(),
|
||||||
emailWhitelistEnabled: z.boolean().optional()
|
emailWhitelistEnabled: z.boolean().optional(),
|
||||||
|
isBaseDomain: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
|
@ -45,15 +46,28 @@ const updateResourceBodySchema = z
|
||||||
},
|
},
|
||||||
{ message: "Cannot update proxyPort" }
|
{ message: "Cannot update proxyPort" }
|
||||||
)
|
)
|
||||||
|
// .refine(
|
||||||
|
// (data) => {
|
||||||
|
// if (data.proxyPort === 443 || data.proxyPort === 80) {
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
// return true;
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// message: "Port 80 and 443 are reserved for http and https resources"
|
||||||
|
// }
|
||||||
|
// )
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.proxyPort === 443 || data.proxyPort === 80) {
|
if (!config.getRawConfig().flags?.allow_base_domain_resources) {
|
||||||
return false;
|
if (data.isBaseDomain) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
message: "Port 80 and 443 are reserved for http and https resources"
|
message: "Base domain resources are not allowed"
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -104,6 +118,29 @@ export async function updateResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (updateData.subdomain) {
|
||||||
|
if (!resource.http) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Cannot update subdomain for non-http resource"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const valid = subdomainSchema.safeParse(
|
||||||
|
updateData.subdomain
|
||||||
|
).success;
|
||||||
|
if (!valid) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
"Invalid subdomain provided"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (updateData.proxyPort) {
|
if (updateData.proxyPort) {
|
||||||
const proxyPort = updateData.proxyPort;
|
const proxyPort = updateData.proxyPort;
|
||||||
const existingResource = await db
|
const existingResource = await db
|
||||||
|
@ -138,15 +175,32 @@ export async function updateResource(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullDomain = updateData.subdomain
|
let fullDomain = "";
|
||||||
? `${updateData.subdomain}.${org.domain}`
|
if (updateData.isBaseDomain) {
|
||||||
: undefined;
|
fullDomain = org.domain;
|
||||||
|
} else {
|
||||||
|
fullDomain = `${updateData.subdomain}.${org.domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
const updatePayload = {
|
const updatePayload = {
|
||||||
...updateData,
|
...updateData,
|
||||||
...(fullDomain && { fullDomain })
|
...(fullDomain && { fullDomain })
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [existingDomain] = await db
|
||||||
|
.select()
|
||||||
|
.from(resources)
|
||||||
|
.where(eq(resources.fullDomain, fullDomain));
|
||||||
|
|
||||||
|
if (existingDomain && existingDomain.resourceId !== resourceId) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.CONFLICT,
|
||||||
|
"Resource with that domain already exists"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const updatedResource = await db
|
const updatedResource = await db
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set(updatePayload)
|
.set(updatePayload)
|
||||||
|
|
|
@ -25,6 +25,7 @@ export async function traefikConfigProvider(
|
||||||
http: resources.http,
|
http: resources.http,
|
||||||
proxyPort: resources.proxyPort,
|
proxyPort: resources.proxyPort,
|
||||||
protocol: resources.protocol,
|
protocol: resources.protocol,
|
||||||
|
isBaseDomain: resources.isBaseDomain,
|
||||||
// Site fields
|
// Site fields
|
||||||
site: {
|
site: {
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
|
@ -110,11 +111,11 @@ export async function traefikConfigProvider(
|
||||||
|
|
||||||
const routerName = `${resource.resourceId}-router`;
|
const routerName = `${resource.resourceId}-router`;
|
||||||
const serviceName = `${resource.resourceId}-service`;
|
const serviceName = `${resource.resourceId}-service`;
|
||||||
const fullDomain = `${resource.subdomain}.${org.domain}`;
|
const fullDomain = `${resource.fullDomain}`;
|
||||||
|
|
||||||
if (resource.http) {
|
if (resource.http) {
|
||||||
// HTTP configuration remains the same
|
// HTTP configuration remains the same
|
||||||
if (!resource.subdomain) {
|
if (!resource.subdomain && !resource.isBaseDomain) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -148,6 +149,8 @@ export async function traefikConfigProvider(
|
||||||
: {})
|
: {})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
logger.debug(config.getRawConfig().traefik.prefer_wildcard_cert)
|
||||||
|
|
||||||
const additionalMiddlewares =
|
const additionalMiddlewares =
|
||||||
config.getRawConfig().traefik.additional_middlewares || [];
|
config.getRawConfig().traefik.additional_middlewares || [];
|
||||||
|
|
||||||
|
|
|
@ -23,7 +23,12 @@ export async function copyInConfig() {
|
||||||
const allResources = await trx.select().from(resources);
|
const allResources = await trx.select().from(resources);
|
||||||
|
|
||||||
for (const resource of allResources) {
|
for (const resource of allResources) {
|
||||||
const fullDomain = `${resource.subdomain}.${domain}`;
|
let fullDomain = "";
|
||||||
|
if (resource.isBaseDomain) {
|
||||||
|
fullDomain = domain;
|
||||||
|
} else {
|
||||||
|
fullDomain = `${resource.subdomain}.${domain}`;
|
||||||
|
}
|
||||||
await trx
|
await trx
|
||||||
.update(resources)
|
.update(resources)
|
||||||
.set({ fullDomain })
|
.set({ fullDomain })
|
||||||
|
|
|
@ -3,8 +3,9 @@ import db, { exists } from "@server/db";
|
||||||
import path from "path";
|
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 { __DIRNAME, APP_VERSION } from "@server/lib/consts";
|
import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts";
|
||||||
import { SqliteError } from "better-sqlite3";
|
import { SqliteError } from "better-sqlite3";
|
||||||
|
import fs from "fs";
|
||||||
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";
|
||||||
import m3 from "./scripts/1.0.0-beta3";
|
import m3 from "./scripts/1.0.0-beta3";
|
||||||
|
@ -12,6 +13,7 @@ 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";
|
import m7 from "./scripts/1.0.0-beta10";
|
||||||
|
import m8 from "./scripts/1.0.0-beta12";
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -24,12 +26,41 @@ const migrations = [
|
||||||
{ 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 }
|
{ version: "1.0.0-beta.10", run: m7 },
|
||||||
|
{ version: "1.0.0-beta.12", run: m8 }
|
||||||
// Add new migrations here as they are created
|
// Add new migrations here as they are created
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Run the migrations
|
await run();
|
||||||
await runMigrations();
|
|
||||||
|
async function run() {
|
||||||
|
// backup the database
|
||||||
|
backupDb();
|
||||||
|
|
||||||
|
// run the migrations
|
||||||
|
await runMigrations();
|
||||||
|
}
|
||||||
|
|
||||||
|
function backupDb() {
|
||||||
|
// make dir config/db/backups
|
||||||
|
const appPath = APP_PATH;
|
||||||
|
const dbDir = path.join(appPath, "db");
|
||||||
|
|
||||||
|
const backupsDir = path.join(dbDir, "backups");
|
||||||
|
|
||||||
|
// check if the backups directory exists and create it if it doesn't
|
||||||
|
if (!fs.existsSync(backupsDir)) {
|
||||||
|
fs.mkdirSync(backupsDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// copy the db.sqlite file to backups
|
||||||
|
// add the date to the filename
|
||||||
|
const date = new Date();
|
||||||
|
const dateString = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}_${date.getHours()}-${date.getMinutes()}-${date.getSeconds()}`;
|
||||||
|
const dbPath = path.join(dbDir, "db.sqlite");
|
||||||
|
const backupPath = path.join(backupsDir, `db_${dateString}.sqlite`);
|
||||||
|
fs.copyFileSync(dbPath, backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
export async function runMigrations() {
|
export async function runMigrations() {
|
||||||
try {
|
try {
|
||||||
|
@ -105,7 +136,10 @@ async function executeScripts() {
|
||||||
`Successfully completed migration ${migration.version}`
|
`Successfully completed migration ${migration.version}`
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
if (
|
||||||
|
e instanceof SqliteError &&
|
||||||
|
e.code === "SQLITE_CONSTRAINT_UNIQUE"
|
||||||
|
) {
|
||||||
console.error("Migration has already run! Skipping...");
|
console.error("Migration has already run! Skipping...");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
62
server/setup/scripts/1.0.0-beta12.ts
Normal file
62
server/setup/scripts/1.0.0-beta12.ts
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import db from "@server/db";
|
||||||
|
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
|
||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import fs from "fs";
|
||||||
|
import yaml from "js-yaml";
|
||||||
|
|
||||||
|
export default async function migration() {
|
||||||
|
console.log("Running setup script 1.0.0-beta.12...");
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (!rawConfig.flags) {
|
||||||
|
rawConfig.flags = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
rawConfig.flags.allow_base_domain_resources = true;
|
||||||
|
|
||||||
|
// Write the updated YAML back to the file
|
||||||
|
const updatedYaml = yaml.dump(rawConfig);
|
||||||
|
fs.writeFileSync(filePath, updatedYaml, "utf8");
|
||||||
|
|
||||||
|
console.log(`Added new config option: allow_base_domain_resources`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log(
|
||||||
|
`Unable to add new config option: allow_base_domain_resources. This is not critical.`
|
||||||
|
);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
db.transaction((trx) => {
|
||||||
|
trx.run(sql`ALTER TABLE 'resources' ADD 'isBaseDomain' integer;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Added new column: isBaseDomain`);
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Unable to add new column: isBaseDomain");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
|
}
|
|
@ -63,6 +63,8 @@ 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";
|
import CopyTextBox from "@app/components/CopyTextBox";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
|
import { Label } from "@app/components/ui/label";
|
||||||
|
|
||||||
const createResourceFormSchema = z
|
const createResourceFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
|
@ -71,7 +73,8 @@ const createResourceFormSchema = z
|
||||||
siteId: z.number(),
|
siteId: z.number(),
|
||||||
http: z.boolean(),
|
http: z.boolean(),
|
||||||
protocol: z.string(),
|
protocol: z.string(),
|
||||||
proxyPort: z.number().optional()
|
proxyPort: z.number().optional(),
|
||||||
|
isBaseDomain: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -92,7 +95,7 @@ const createResourceFormSchema = z
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.http) {
|
if (data.http && !data.isBaseDomain) {
|
||||||
return subdomainSchema.safeParse(data.subdomain).success;
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -131,12 +134,15 @@ export default function CreateResourceForm({
|
||||||
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
|
const [domainSuffix, setDomainSuffix] = useState<string>(org.org.domain);
|
||||||
const [showSnippets, setShowSnippets] = useState(false);
|
const [showSnippets, setShowSnippets] = useState(false);
|
||||||
const [resourceId, setResourceId] = useState<number | null>(null);
|
const [resourceId, setResourceId] = useState<number | null>(null);
|
||||||
|
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
||||||
|
"subdomain"
|
||||||
|
);
|
||||||
|
|
||||||
const form = useForm<CreateResourceFormValues>({
|
const form = useForm<CreateResourceFormValues>({
|
||||||
resolver: zodResolver(createResourceFormSchema),
|
resolver: zodResolver(createResourceFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
subdomain: "",
|
subdomain: "",
|
||||||
name: "My Resource",
|
name: "",
|
||||||
http: true,
|
http: true,
|
||||||
protocol: "tcp"
|
protocol: "tcp"
|
||||||
}
|
}
|
||||||
|
@ -180,7 +186,8 @@ export default function CreateResourceForm({
|
||||||
http: data.http,
|
http: data.http,
|
||||||
protocol: data.protocol,
|
protocol: data.protocol,
|
||||||
proxyPort: data.http ? undefined : data.proxyPort,
|
proxyPort: data.http ? undefined : data.proxyPort,
|
||||||
siteId: data.siteId
|
siteId: data.siteId,
|
||||||
|
isBaseDomain: data.isBaseDomain
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
@ -246,7 +253,7 @@ export default function CreateResourceForm({
|
||||||
<FormLabel>Name</FormLabel>
|
<FormLabel>Name</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Your name"
|
placeholder="Resource name"
|
||||||
{...field}
|
{...field}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
@ -291,33 +298,89 @@ export default function CreateResourceForm({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{form.watch("http") &&
|
||||||
|
env.flags.allowBaseDomainResources && (
|
||||||
|
<div>
|
||||||
|
<RadioGroup
|
||||||
|
className="flex space-x-4"
|
||||||
|
defaultValue={domainType}
|
||||||
|
onValueChange={(val) => {
|
||||||
|
setDomainType(
|
||||||
|
val as any
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"isBaseDomain",
|
||||||
|
val === "basedomain"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="subdomain"
|
||||||
|
id="r1"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="r1">
|
||||||
|
Subdomain
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem
|
||||||
|
value="basedomain"
|
||||||
|
id="r2"
|
||||||
|
/>
|
||||||
|
<Label htmlFor="r2">
|
||||||
|
Base Domain
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{form.watch("http") && (
|
{form.watch("http") && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="subdomain"
|
name="subdomain"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
{!env.flags
|
||||||
Subdomain
|
.allowBaseDomainResources && (
|
||||||
</FormLabel>
|
<FormLabel>
|
||||||
<FormControl>
|
Subdomain
|
||||||
<CustomDomainInput
|
</FormLabel>
|
||||||
value={
|
)}
|
||||||
field.value ??
|
{domainType ===
|
||||||
""
|
"subdomain" ? (
|
||||||
}
|
<FormControl>
|
||||||
domainSuffix={
|
<CustomDomainInput
|
||||||
domainSuffix
|
value={
|
||||||
}
|
field.value ??
|
||||||
placeholder="Enter subdomain"
|
""
|
||||||
onChange={(value) =>
|
}
|
||||||
form.setValue(
|
domainSuffix={
|
||||||
"subdomain",
|
domainSuffix
|
||||||
|
}
|
||||||
|
placeholder="Subdomain"
|
||||||
|
onChange={(
|
||||||
value
|
value
|
||||||
)
|
) =>
|
||||||
}
|
form.setValue(
|
||||||
/>
|
"subdomain",
|
||||||
</FormControl>
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
domainSuffix
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
This is the fully
|
This is the fully
|
||||||
qualified domain name
|
qualified domain name
|
||||||
|
@ -471,9 +534,7 @@ export default function CreateResourceForm({
|
||||||
site
|
site
|
||||||
) => (
|
) => (
|
||||||
<CommandItem
|
<CommandItem
|
||||||
value={
|
value={`${site.siteId}:${site.name}:${site.niceId}`}
|
||||||
`${site.siteId}:${site.name}:${site.niceId}`
|
|
||||||
}
|
|
||||||
key={
|
key={
|
||||||
site.siteId
|
site.siteId
|
||||||
}
|
}
|
||||||
|
@ -567,21 +628,25 @@ export default function CreateResourceForm({
|
||||||
)}
|
)}
|
||||||
</CredenzaBody>
|
</CredenzaBody>
|
||||||
<CredenzaFooter>
|
<CredenzaFooter>
|
||||||
{!showSnippets && <Button
|
{!showSnippets && (
|
||||||
type="submit"
|
<Button
|
||||||
form="create-resource-form"
|
type="submit"
|
||||||
loading={loading}
|
form="create-resource-form"
|
||||||
disabled={loading}
|
loading={loading}
|
||||||
>
|
disabled={loading}
|
||||||
Create Resource
|
>
|
||||||
</Button>}
|
Create Resource
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
{showSnippets && <Button
|
{showSnippets && (
|
||||||
loading={loading}
|
<Button
|
||||||
onClick={() => goToResource()}
|
loading={loading}
|
||||||
>
|
onClick={() => goToResource()}
|
||||||
Go to Resource
|
>
|
||||||
</Button>}
|
Go to Resource
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<CredenzaClose asChild>
|
<CredenzaClose asChild>
|
||||||
<Button variant="outline">Close</Button>
|
<Button variant="outline">Close</Button>
|
||||||
|
|
|
@ -38,7 +38,7 @@ export type ResourceRow = {
|
||||||
domain: string;
|
domain: string;
|
||||||
site: string;
|
site: string;
|
||||||
siteId: string;
|
siteId: string;
|
||||||
hasAuth: boolean;
|
authState: string;
|
||||||
http: boolean;
|
http: boolean;
|
||||||
protocol: string;
|
protocol: string;
|
||||||
proxyPort: number | null;
|
proxyPort: number | null;
|
||||||
|
@ -165,9 +165,7 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
header: "Protocol",
|
header: "Protocol",
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return <span>{resourceRow.protocol.toUpperCase()}</span>;
|
||||||
<span>{resourceRow.protocol.toUpperCase()}</span>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -177,17 +175,23 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{!resourceRow.http ? (
|
{!resourceRow.http ? (
|
||||||
<CopyToClipboard text={resourceRow.proxyPort!.toString()} isLink={false} />
|
<CopyToClipboard
|
||||||
) : (
|
text={resourceRow.proxyPort!.toString()}
|
||||||
<CopyToClipboard text={resourceRow.domain} isLink={true} />
|
isLink={false}
|
||||||
)}
|
/>
|
||||||
|
) : (
|
||||||
|
<CopyToClipboard
|
||||||
|
text={resourceRow.domain}
|
||||||
|
isLink={true}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "hasAuth",
|
accessorKey: "authState",
|
||||||
header: ({ column }) => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
@ -205,23 +209,19 @@ export default function SitesTable({ resources, orgId }: ResourcesTableProps) {
|
||||||
const resourceRow = row.original;
|
const resourceRow = row.original;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
{resourceRow.authState === "protected" ? (
|
||||||
|
<span className="text-green-500 flex items-center space-x-2">
|
||||||
{!resourceRow.http ? (
|
<ShieldCheck className="w-4 h-4" />
|
||||||
|
<span>Protected</span>
|
||||||
|
</span>
|
||||||
|
) : resourceRow.authState === "not_protected" ? (
|
||||||
|
<span className="text-yellow-500 flex items-center space-x-2">
|
||||||
|
<ShieldOff className="w-4 h-4" />
|
||||||
|
<span>Not Protected</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
<span>--</span>
|
<span>--</span>
|
||||||
) :
|
)}
|
||||||
resourceRow.hasAuth ? (
|
|
||||||
<span className="text-green-500 flex items-center space-x-2">
|
|
||||||
<ShieldCheck className="w-4 h-4" />
|
|
||||||
<span>Protected</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-yellow-500 flex items-center space-x-2">
|
|
||||||
<ShieldOff className="w-4 h-4" />
|
|
||||||
<span>Not Protected</span>
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,7 @@
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import {
|
import { InfoIcon, ShieldCheck, ShieldOff } from "lucide-react";
|
||||||
InfoIcon,
|
|
||||||
ShieldCheck,
|
|
||||||
ShieldOff
|
|
||||||
} from "lucide-react";
|
|
||||||
import { useOrgContext } from "@app/hooks/useOrgContext";
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
import { useResourceContext } from "@app/hooks/useResourceContext";
|
import { useResourceContext } from "@app/hooks/useResourceContext";
|
||||||
import { Separator } from "@app/components/ui/separator";
|
import { Separator } from "@app/components/ui/separator";
|
||||||
|
@ -26,9 +22,12 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const { resource, authInfo } = useResourceContext();
|
const { resource, authInfo } = useResourceContext();
|
||||||
|
|
||||||
const fullUrl = `${resource.ssl ? "https" : "http"}://${
|
let fullUrl = `${resource.ssl ? "https" : "http"}://`;
|
||||||
resource.subdomain
|
if (resource.isBaseDomain) {
|
||||||
}.${org.org.domain}`;
|
fullUrl = fullUrl + org.org.domain;
|
||||||
|
} else {
|
||||||
|
fullUrl = fullUrl + `${resource.subdomain}.${org.org.domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Alert>
|
<Alert>
|
||||||
|
@ -82,7 +81,9 @@ export default function ResourceInfoBox({}: ResourceInfoBoxType) {
|
||||||
<InfoSection>
|
<InfoSection>
|
||||||
<InfoSectionTitle>Protocol</InfoSectionTitle>
|
<InfoSectionTitle>Protocol</InfoSectionTitle>
|
||||||
<InfoSectionContent>
|
<InfoSectionContent>
|
||||||
<span>{resource.protocol.toUpperCase()}</span>
|
<span>
|
||||||
|
{resource.protocol.toUpperCase()}
|
||||||
|
</span>
|
||||||
</InfoSectionContent>
|
</InfoSectionContent>
|
||||||
</InfoSection>
|
</InfoSection>
|
||||||
<Separator orientation="vertical" />
|
<Separator orientation="vertical" />
|
||||||
|
|
|
@ -51,13 +51,17 @@ import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
import { subdomainSchema } from "@server/schemas/subdomainSchema";
|
||||||
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
import { CaretSortIcon, CheckIcon } from "@radix-ui/react-icons";
|
||||||
|
import { pullEnv } from "@app/lib/pullEnv";
|
||||||
|
import { RadioGroup, RadioGroupItem } from "@app/components/ui/radio-group";
|
||||||
|
import { Label } from "@app/components/ui/label";
|
||||||
|
|
||||||
const GeneralFormSchema = z
|
const GeneralFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
subdomain: z.string().optional(),
|
subdomain: z.string().optional(),
|
||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
proxyPort: z.number().optional(),
|
proxyPort: z.number().optional(),
|
||||||
http: z.boolean()
|
http: z.boolean(),
|
||||||
|
isBaseDomain: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
@ -78,7 +82,7 @@ const GeneralFormSchema = z
|
||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
if (data.http) {
|
if (data.http && !data.isBaseDomain) {
|
||||||
return subdomainSchema.safeParse(data.subdomain).success;
|
return subdomainSchema.safeParse(data.subdomain).success;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -103,9 +107,11 @@ export default function GeneralForm() {
|
||||||
const { org } = useOrgContext();
|
const { org } = useOrgContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const orgId = params.orgId;
|
const orgId = params.orgId;
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient({ env });
|
||||||
|
|
||||||
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
const [sites, setSites] = useState<ListSitesResponse["sites"]>([]);
|
||||||
const [saveLoading, setSaveLoading] = useState(false);
|
const [saveLoading, setSaveLoading] = useState(false);
|
||||||
|
@ -113,13 +119,18 @@ export default function GeneralForm() {
|
||||||
const [transferLoading, setTransferLoading] = useState(false);
|
const [transferLoading, setTransferLoading] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
const [domainType, setDomainType] = useState<"subdomain" | "basedomain">(
|
||||||
|
resource.isBaseDomain ? "basedomain" : "subdomain"
|
||||||
|
);
|
||||||
|
|
||||||
const form = useForm<GeneralFormValues>({
|
const form = useForm<GeneralFormValues>({
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: resource.name,
|
name: resource.name,
|
||||||
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
subdomain: resource.subdomain ? resource.subdomain : undefined,
|
||||||
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
|
proxyPort: resource.proxyPort ? resource.proxyPort : undefined,
|
||||||
http: resource.http
|
http: resource.http,
|
||||||
|
isBaseDomain: resource.isBaseDomain ? true : false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
|
@ -148,7 +159,8 @@ export default function GeneralForm() {
|
||||||
.post(`resource/${resource?.resourceId}`, {
|
.post(`resource/${resource?.resourceId}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
proxyPort: data.proxyPort
|
proxyPort: data.proxyPort,
|
||||||
|
isBaseDomain: data.isBaseDomain
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
toast({
|
toast({
|
||||||
|
@ -170,7 +182,8 @@ export default function GeneralForm() {
|
||||||
updateResource({
|
updateResource({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
subdomain: data.subdomain,
|
subdomain: data.subdomain,
|
||||||
proxyPort: data.proxyPort
|
proxyPort: data.proxyPort,
|
||||||
|
isBaseDomain: data.isBaseDomain
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setSaveLoading(false);
|
setSaveLoading(false);
|
||||||
|
@ -242,40 +255,103 @@ export default function GeneralForm() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{resource.http ? (
|
{resource.http && (
|
||||||
<FormField
|
<>
|
||||||
control={form.control}
|
{env.flags.allowBaseDomainResources && (
|
||||||
name="subdomain"
|
<div>
|
||||||
render={({ field }) => (
|
<RadioGroup
|
||||||
<FormItem>
|
className="flex space-x-4"
|
||||||
<FormLabel>Subdomain</FormLabel>
|
defaultValue={domainType}
|
||||||
<FormControl>
|
onValueChange={(val) => {
|
||||||
<CustomDomainInput
|
setDomainType(
|
||||||
value={
|
val as any
|
||||||
field.value || ""
|
);
|
||||||
}
|
form.setValue(
|
||||||
domainSuffix={
|
"isBaseDomain",
|
||||||
domainSuffix
|
val === "basedomain"
|
||||||
}
|
);
|
||||||
placeholder="Enter subdomain"
|
}}
|
||||||
onChange={(value) =>
|
>
|
||||||
form.setValue(
|
<div className="flex items-center space-x-2">
|
||||||
"subdomain",
|
<RadioGroupItem
|
||||||
value
|
value="subdomain"
|
||||||
)
|
id="r1"
|
||||||
}
|
/>
|
||||||
/>
|
<Label htmlFor="r1">
|
||||||
</FormControl>
|
Subdomain
|
||||||
<FormDescription>
|
</Label>
|
||||||
This is the subdomain that
|
</div>
|
||||||
will be used to access the
|
<div className="flex items-center space-x-2">
|
||||||
resource.
|
<RadioGroupItem
|
||||||
</FormDescription>
|
value="basedomain"
|
||||||
<FormMessage />
|
id="r2"
|
||||||
</FormItem>
|
/>
|
||||||
|
<Label htmlFor="r2">
|
||||||
|
Base Domain
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
</RadioGroup>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
|
||||||
) : (
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="subdomain"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
{!env.flags
|
||||||
|
.allowBaseDomainResources && (
|
||||||
|
<FormLabel>
|
||||||
|
Subdomain
|
||||||
|
</FormLabel>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{domainType ===
|
||||||
|
"subdomain" ? (
|
||||||
|
<FormControl>
|
||||||
|
<CustomDomainInput
|
||||||
|
value={
|
||||||
|
field.value ||
|
||||||
|
""
|
||||||
|
}
|
||||||
|
domainSuffix={
|
||||||
|
domainSuffix
|
||||||
|
}
|
||||||
|
placeholder="Enter subdomain"
|
||||||
|
onChange={(
|
||||||
|
value
|
||||||
|
) =>
|
||||||
|
form.setValue(
|
||||||
|
"subdomain",
|
||||||
|
value
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
) : (
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
value={
|
||||||
|
domainSuffix
|
||||||
|
}
|
||||||
|
readOnly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
<FormDescription>
|
||||||
|
This is the subdomain
|
||||||
|
that will be used to
|
||||||
|
access the resource.
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!resource.http && (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="proxyPort"
|
name="proxyPort"
|
||||||
|
|
|
@ -56,11 +56,14 @@ export default async function ResourcesPage(props: ResourcesPageProps) {
|
||||||
protocol: resource.protocol,
|
protocol: resource.protocol,
|
||||||
proxyPort: resource.proxyPort,
|
proxyPort: resource.proxyPort,
|
||||||
http: resource.http,
|
http: resource.http,
|
||||||
hasAuth:
|
authState: !resource.http
|
||||||
resource.sso ||
|
? "none"
|
||||||
resource.pincodeId !== null ||
|
: resource.sso ||
|
||||||
resource.pincodeId !== null ||
|
resource.pincodeId !== null ||
|
||||||
resource.whitelist
|
resource.pincodeId !== null ||
|
||||||
|
resource.whitelist
|
||||||
|
? "protected"
|
||||||
|
: "not_protected"
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -75,6 +75,7 @@ export default function CreateSiteForm({
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
|
const { env } = useEnvContext();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isChecked, setIsChecked] = useState(false);
|
const [isChecked, setIsChecked] = useState(false);
|
||||||
|
@ -234,13 +235,7 @@ Endpoint = ${siteDefaults.endpoint}:${siteDefaults.listenPort}
|
||||||
PersistentKeepalive = 5`
|
PersistentKeepalive = 5`
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
// am I at http or https?
|
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${env.app.dashboardUrl}`;
|
||||||
let proto = "https:";
|
|
||||||
// if (typeof window !== "undefined") {
|
|
||||||
// proto = window.location.protocol;
|
|
||||||
// }
|
|
||||||
|
|
||||||
const newtConfig = `newt --id ${siteDefaults?.newtId} --secret ${siteDefaults?.newtSecret} --endpoint ${proto}//${siteDefaults?.endpoint}`;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
|
|
@ -1,30 +1,53 @@
|
||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||||
import { Check } from "lucide-react"
|
import { Check } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@app/lib/cn"
|
import { cn } from "@app/lib/cn";
|
||||||
|
|
||||||
const Checkbox = React.forwardRef<
|
const Checkbox = React.forwardRef<
|
||||||
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
React.ElementRef<typeof CheckboxPrimitive.Root>,
|
||||||
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
||||||
>(({ className, ...props }, ref) => (
|
>(({ className, ...props }, ref) => (
|
||||||
<CheckboxPrimitive.Root
|
<CheckboxPrimitive.Root
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
|
||||||
<CheckboxPrimitive.Indicator
|
|
||||||
className={cn("flex items-center justify-center text-current")}
|
|
||||||
>
|
>
|
||||||
<Check className="h-4 w-4" />
|
<CheckboxPrimitive.Indicator
|
||||||
</CheckboxPrimitive.Indicator>
|
className={cn("flex items-center justify-center text-current")}
|
||||||
</CheckboxPrimitive.Root>
|
>
|
||||||
))
|
<Check className="h-4 w-4" />
|
||||||
Checkbox.displayName = CheckboxPrimitive.Root.displayName
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
));
|
||||||
|
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
||||||
|
|
||||||
export { Checkbox }
|
interface CheckboxWithLabelProps
|
||||||
|
extends React.ComponentPropsWithoutRef<typeof Checkbox> {
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CheckboxWithLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof Checkbox>,
|
||||||
|
CheckboxWithLabelProps
|
||||||
|
>(({ className, label, id, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<div className={cn("flex items-center space-x-2", className)}>
|
||||||
|
<Checkbox id={id} ref={ref} {...props} />
|
||||||
|
<label
|
||||||
|
htmlFor={id}
|
||||||
|
className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
CheckboxWithLabel.displayName = "CheckboxWithLabel";
|
||||||
|
|
||||||
|
export { Checkbox, CheckboxWithLabel };
|
||||||
|
|
|
@ -6,12 +6,15 @@ export function pullEnv(): Env {
|
||||||
nextPort: process.env.NEXT_PORT as string,
|
nextPort: process.env.NEXT_PORT as string,
|
||||||
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
externalPort: process.env.SERVER_EXTERNAL_PORT as string,
|
||||||
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
|
sessionCookieName: process.env.SESSION_COOKIE_NAME as string,
|
||||||
resourceAccessTokenParam: process.env.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
resourceAccessTokenParam: process.env
|
||||||
resourceSessionRequestParam: process.env.RESOURCE_SESSION_REQUEST_PARAM as string
|
.RESOURCE_ACCESS_TOKEN_PARAM as string,
|
||||||
|
resourceSessionRequestParam: process.env
|
||||||
|
.RESOURCE_SESSION_REQUEST_PARAM as string
|
||||||
},
|
},
|
||||||
app: {
|
app: {
|
||||||
environment: process.env.ENVIRONMENT as string,
|
environment: process.env.ENVIRONMENT as string,
|
||||||
version: process.env.APP_VERSION as string
|
version: process.env.APP_VERSION as string,
|
||||||
|
dashboardUrl: process.env.DASHBOARD_URL as string,
|
||||||
},
|
},
|
||||||
email: {
|
email: {
|
||||||
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
|
emailEnabled: process.env.EMAIL_ENABLED === "true" ? true : false
|
||||||
|
@ -29,6 +32,10 @@ export function pullEnv(): Env {
|
||||||
: false,
|
: false,
|
||||||
allowRawResources:
|
allowRawResources:
|
||||||
process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
|
process.env.FLAGS_ALLOW_RAW_RESOURCES === "true" ? true : false,
|
||||||
|
allowBaseDomainResources:
|
||||||
|
process.env.FLAGS_ALLOW_BASE_DOMAIN_RESOURCES === "true"
|
||||||
|
? true
|
||||||
|
: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ export type Env = {
|
||||||
app: {
|
app: {
|
||||||
environment: string;
|
environment: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
dashboardUrl: string;
|
||||||
},
|
},
|
||||||
server: {
|
server: {
|
||||||
externalPort: string;
|
externalPort: string;
|
||||||
|
@ -18,5 +19,6 @@ export type Env = {
|
||||||
disableUserCreateOrg: boolean;
|
disableUserCreateOrg: boolean;
|
||||||
emailVerificationRequired: boolean;
|
emailVerificationRequired: boolean;
|
||||||
allowRawResources: boolean;
|
allowRawResources: boolean;
|
||||||
|
allowBaseDomainResources: boolean;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue