Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apps/control-panel-app/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=kubeara-dev
DB_SSL=false
ENCRYPTION_SECRET=change-me-to-a-long-random-secret
JWT_SECRET=change-me-jwt-secret
JWT_REFRESH_SECRET=change-me-jwt-refresh-secret
Expand All @@ -19,8 +20,9 @@ SMTP_SERVER=smtp-relay.brevo.com
SMTP_PORT=587
SMTP_LOGIN=your-smtp-login@example.com
SMTP_PASSWORD=your_smtp_password
BREVO_API_KEY=your_brevo_api_key
BREVO_FROM_EMAIL=noreply@example.com
# Optional: leave empty to disable email delivery without blocking startup.
BREVO_API_KEY=
BREVO_FROM_EMAIL=
BREVO_FROM_NAME=Kubeara

# Zoho Desk (POST /api/public/support and /api/public/service-requests β€” creates tickets in Zoho Desk; nothing stored locally)
Expand All @@ -29,9 +31,7 @@ ZOHO_CLIENT_SECRET=your_zoho_client_secret
ZOHO_REFRESH_TOKEN=your_zoho_refresh_token
ZOHO_ORGANIZATION_ID=your_zoho_organization_id
ZOHO_DEPARTMENT_ID=your_zoho_department_id
# e.g. https://accounts.zoho.com | https://accounts.zoho.eu | https://accounts.zoho.in
ZOHO_ACCOUNTS_BASE_URL=https://accounts.zoho.in
# e.g. https://desk.zoho.com/api/v1 | https://desk.zoho.eu/api/v1
ZOHO_DESK_BASE_URL=https://desk.zoho.in/api/v1

OTP_EXPIRES_IN=2m
Expand Down
4 changes: 2 additions & 2 deletions apps/control-panel-app/config/typeorm.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as path from "path";
import * as dotenv from "dotenv";
import { DataSource } from "typeorm";
import { isProductionEnv } from "../src/constants/env.constant";
import { isDbSslEnabled } from "../src/constants/env.constant";

dotenv.config({
path: path.join(process.cwd(), "apps", "control-panel-app", ".env"),
Expand All @@ -21,7 +21,7 @@ export default new DataSource({
password: getRequiredEnv("DB_PASSWORD"),
database: getRequiredEnv("DB_DATABASE"),
synchronize: false,
...(isProductionEnv(getRequiredEnv("NODE_ENV"))
...(isDbSslEnabled(getRequiredEnv("DB_SSL"))
? { ssl: { rejectUnauthorized: false } }
: {}),
entities: [
Expand Down
47 changes: 27 additions & 20 deletions apps/control-panel-app/deploy/.env.control-panel.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
#
# ENCRYPTION_SECRET: use the same value on the agent (.env.agent).

# Compose Postgres has no TLS. Keep development so Hub images connect without SSL.
NODE_ENV=development

KUBEARA_CONTROL_PANEL_IMAGE=kubeara/control-panel:prod
KUBEARA_CONSOLE_IMAGE=kubeara/console:prod
# linux/amd64 on Apple Silicon until multi-arch images are pulled; use linux/arm64 when available
Expand All @@ -19,46 +22,50 @@ ENCRYPTION_SECRET=change-me-to-a-long-random-secret
JWT_SECRET=change-me-jwt-secret
JWT_REFRESH_SECRET=change-me-jwt-refresh-secret

# HTTP-only cookie authentication
# HTTP-only cookie authentication (COOKIE_SECURE=false for local HTTP)
ACCESS_TOKEN_COOKIE_NAME=kubeara_access_token
REFRESH_TOKEN_COOKIE_NAME=kubeara_refresh_token
ACCESS_TOKEN_EXPIRES_IN=15m
REFRESH_TOKEN_EXPIRES_IN=7d
COOKIE_DOMAIN=
COOKIE_SECURE=true
COOKIE_SECURE=false
COOKIE_SAME_SITE=lax

OTP_EXPIRES_IN=2m

# Browser origins for public marketing APIs (/api/public/*)
PUBLIC_API_ALLOWED_ORIGINS=https://kubeara.dev,https://www.kubeara.dev,https://app.kubeara.dev,http://localhost:3000
PUBLIC_API_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000,http://127.0.0.1:8080

# Optional: broader list for console/auth (defaults to PUBLIC_API_ALLOWED_ORIGINS)
CORS_ALLOWED_ORIGINS=https://kubeara.dev,https://www.kubeara.dev,https://app.kubeara.dev
CORS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://127.0.0.1:3000,http://127.0.0.1:8080
# Public URL remote agents use to reach this control panel (required for onboard agent install).
CONTROL_PANEL_URL=http://your-control-panel-host:3000
CONTROL_PANEL_URL=http://localhost:3000

# Stripe subscription billing
STRIPE_PUBLISHABLE_KEY=pk_test_your_stripe_publishable_key
STRIPE_SECRET_KEY=sk_test_your_stripe_secret_key
STRIPE_WEBHOOK_SECRET=whsec_your_stripe_webhook_secret
STRIPE_CHECKOUT_PAYMENT_METHODS=card
# Optional email β€” empty keys keep older Hub images (getOrThrow BREVO_*) from crashing
BREVO_API_KEY=
BREVO_FROM_EMAIL=
BREVO_FROM_NAME=Kubeara

# Zoho Desk (POST /api/public/support and /api/public/service-requests β€” creates tickets in Zoho Desk; nothing stored locally)
ZOHO_CLIENT_ID=your_zoho_client_id
ZOHO_CLIENT_SECRET=your_zoho_client_secret
ZOHO_REFRESH_TOKEN=your_zoho_refresh_token
ZOHO_ORGANIZATION_ID=your_zoho_organization_id
ZOHO_DEPARTMENT_ID=your_zoho_department_id
# e.g. https://accounts.zoho.com | https://accounts.zoho.eu | https://accounts.zoho.in
ZOHO_ACCOUNTS_BASE_URL=https://accounts.zoho.in
# e.g. https://desk.zoho.com/api/v1 | https://desk.zoho.eu/api/v1
ZOHO_DESK_BASE_URL=https://desk.zoho.in/api/v1
# Stripe / Zoho optional β€” leave as placeholders or unset; integrations soft-disable
# STRIPE_PUBLISHABLE_KEY=
# STRIPE_SECRET_KEY=
# STRIPE_WEBHOOK_SECRET=
# STRIPE_CHECKOUT_PAYMENT_METHODS=card
# ZOHO_CLIENT_ID=
# ZOHO_CLIENT_SECRET=
# ZOHO_REFRESH_TOKEN=
# ZOHO_ORGANIZATION_ID=
# ZOHO_DEPARTMENT_ID=
# ZOHO_ACCOUNTS_BASE_URL=https://accounts.zoho.in
# ZOHO_DESK_BASE_URL=https://desk.zoho.in/api/v1

# Values below are used inside containers (DB_HOST must be the compose service name).
DB_HOST=postgres
DB_PORT=5432
DB_USERNAME=postgres
DB_PASSWORD=postgres
DB_DATABASE=kubeara
DB_SSL=false

# Grafana Cloud Loki (optional β€” see deploy/.env.monitoring.example)
# GRAFANA_CLOUD_LOKI_URL=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ services:
env_file:
- .env.control-panel
environment:
NODE_ENV: production
NODE_ENV: ${NODE_ENV:-production}
DOCKER_ENV: "true"
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: 5432
DB_USERNAME: ${DB_USERNAME:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_DATABASE: ${DB_DATABASE:-kubeara}
DB_SSL: ${DB_SSL:-false}
ENCRYPTION_SECRET: ${ENCRYPTION_SECRET:?Set ENCRYPTION_SECRET in .env.control-panel}
volumes:
- ./.env.control-panel:/app/apps/control-panel-app/.env:ro
Expand All @@ -68,24 +69,24 @@ services:
env_file:
- .env.control-panel
environment:
NODE_ENV: production
NODE_ENV: ${NODE_ENV:-production}
DOCKER_ENV: "true"
PORT: ${PORT:-3000}
DB_HOST: ${DB_HOST:-postgres}
DB_PORT: 5432
DB_USERNAME: ${DB_USERNAME:-postgres}
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_DATABASE: ${DB_DATABASE:-kubeara}
DB_SSL: ${DB_SSL:-false}
ENCRYPTION_SECRET: ${ENCRYPTION_SECRET:?Set ENCRYPTION_SECRET in .env.control-panel}
# Defaults for local/dev; install.sh replaces with openssl-generated secrets. Override in .env.control-panel for production.
JWT_SECRET: ${JWT_SECRET:-change-me-jwt-secret}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-change-me-jwt-refresh-secret}
ACCESS_TOKEN_COOKIE_NAME: ${ACCESS_TOKEN_COOKIE_NAME:-kubeara_access_token}
REFRESH_TOKEN_COOKIE_NAME: ${REFRESH_TOKEN_COOKIE_NAME:-kubeara_refresh_token}
ACCESS_TOKEN_EXPIRES_IN: ${ACCESS_TOKEN_EXPIRES_IN:-15m}
REFRESH_TOKEN_EXPIRES_IN: ${REFRESH_TOKEN_EXPIRES_IN:-7d}
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
COOKIE_SECURE: ${COOKIE_SECURE:-true}
COOKIE_SECURE: ${COOKIE_SECURE:-false}
COOKIE_SAME_SITE: ${COOKIE_SAME_SITE:-lax}
CONTROL_PANEL_URL: ${CONTROL_PANEL_URL:-}
KUBEARA_AGENT_IMAGE: ${KUBEARA_AGENT_IMAGE:-kubeara/agent:prod}
Expand Down
1 change: 1 addition & 0 deletions apps/control-panel-app/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ services:
DB_PASSWORD: ${DB_PASSWORD:-postgres}
DB_DATABASE: ${DB_DATABASE:-templates}
ENCRYPTION_SECRET: ${ENCRYPTION_SECRET}
DB_SSL: ${DB_SSL:-false}
depends_on:
postgres:
condition: service_healthy
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from "typeorm";

export class SelfHostInstallations1785845114404 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: "selfHostInstallations",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
generationStrategy: "uuid",
default: "uuid_generate_v4()",
},
{
name: "installationId",
type: "uuid",
isNullable: false,
},
{
name: "eventType",
type: "varchar",
length: "32",
isNullable: false,
},
{
name: "version",
type: "varchar",
length: "64",
isNullable: false,
},
{
name: "previousVersion",
type: "varchar",
length: "64",
isNullable: true,
},
{
name: "ipAddress",
type: "varchar",
length: "255",
isNullable: false,
},
{
name: "userAgent",
type: "varchar",
length: "512",
isNullable: true,
},
{
name: "os",
type: "varchar",
length: "128",
isNullable: true,
},
{
name: "osVersion",
type: "varchar",
length: "128",
isNullable: true,
},
{
name: "architecture",
type: "varchar",
length: "64",
isNullable: true,
},
{
name: "dockerVersion",
type: "varchar",
length: "64",
isNullable: true,
},
{
name: "composeVersion",
type: "varchar",
length: "64",
isNullable: true,
},
{
name: "status",
type: "varchar",
length: "50",
default: "'ACTIVE'",
isNullable: false,
},
{
name: "metadata",
type: "jsonb",
isNullable: true,
},
{
name: "createdAt",
type: "bigint",
isNullable: false,
},
{
name: "updatedAt",
type: "bigint",
isNullable: false,
},
{
name: "deletedAt",
type: "bigint",
isNullable: true,
},
],
}),
true,
);

await queryRunner.createIndex(
"selfHostInstallations",
new TableIndex({
name: "IDX_self_host_installations_installationId",
columnNames: ["installationId"],
}),
);

await queryRunner.createIndex(
"selfHostInstallations",
new TableIndex({
name: "IDX_self_host_installations_eventType",
columnNames: ["eventType"],
}),
);

await queryRunner.createIndex(
"selfHostInstallations",
new TableIndex({
name: "IDX_self_host_installations_createdAt",
columnNames: ["createdAt"],
}),
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex(
"selfHostInstallations",
"IDX_self_host_installations_createdAt",
);
await queryRunner.dropIndex(
"selfHostInstallations",
"IDX_self_host_installations_eventType",
);
await queryRunner.dropIndex(
"selfHostInstallations",
"IDX_self_host_installations_installationId",
);

await queryRunner.dropTable("selfHostInstallations");
}
}
6 changes: 3 additions & 3 deletions apps/control-panel-app/seeders/billing-cycles.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import dayjs from "dayjs";
import { BillingCycleEntity } from "../src/modules/subscriptions/entities/billing-cycle.entity";
import { BillingCycleSlug } from "../src/modules/subscriptions/enums/billing-cycle.enum";
import { EntityStatus } from "../src/common/entity/base.entity";
import { isProductionEnv } from "../src/constants/env.constant";
import { isDbSslEnabled } from "../src/constants/env.constant";

const ROOT_DIR = process.cwd();
const APP_ENV_PATH = path.join(ROOT_DIR, "apps/control-panel-app/.env");
Expand Down Expand Up @@ -53,7 +53,7 @@ function loadEnv(): ConfigService {

export async function seedBillingCycles(): Promise<void> {
const configService = loadEnv();
const isProduction = isProductionEnv(configService.get<string>("NODE_ENV"));
const useSsl = isDbSslEnabled(configService.get<string>("DB_SSL"));
const ds = new DataSource({
type: "postgres",
host: configService.get<string>("DB_HOST"),
Expand All @@ -63,7 +63,7 @@ export async function seedBillingCycles(): Promise<void> {
database: configService.get<string>("DB_DATABASE"),
entities: [BillingCycleEntity],
synchronize: false,
...(isProduction ? { ssl: { rejectUnauthorized: false } } : {}),
...(useSsl ? { ssl: { rejectUnauthorized: false } } : {}),
});

if (!ds.isInitialized) {
Expand Down
6 changes: 3 additions & 3 deletions apps/control-panel-app/seeders/plans.seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { PlanEntity } from "../src/modules/subscriptions/entities/plan.entity";
import { PlanSlug } from "../src/modules/subscriptions/enums/plan-slug.enum";
import { EntityStatus } from "../src/common/entity/base.entity";
import { PLAN_DEFINITIONS } from "./plan-definitions.defaults";
import { isProductionEnv } from "../src/constants/env.constant";
import { isDbSslEnabled } from "../src/constants/env.constant";

const ROOT_DIR = process.cwd();
const APP_ENV_PATH = path.join(ROOT_DIR, "apps/control-panel-app/.env");
Expand All @@ -26,7 +26,7 @@ const LEGACY_SLUGS = ["starter", "pro", "max", "business"] as const;

export async function seedPlans(): Promise<void> {
const configService = loadEnv();
const isProduction = isProductionEnv(configService.get<string>("NODE_ENV"));
const useSsl = isDbSslEnabled(configService.get<string>("DB_SSL"));
const ds = new DataSource({
type: "postgres",
host: configService.get<string>("DB_HOST"),
Expand All @@ -36,7 +36,7 @@ export async function seedPlans(): Promise<void> {
database: configService.get<string>("DB_DATABASE"),
entities: [PlanEntity],
synchronize: false,
...(isProduction ? { ssl: { rejectUnauthorized: false } } : {}),
...(useSsl ? { ssl: { rejectUnauthorized: false } } : {}),
});

if (!ds.isInitialized) {
Expand Down
Loading
Loading