Skip to content
Open
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
189 changes: 183 additions & 6 deletions api/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import ConnectionPool from './connection-pool.js';
import { ConnectionWebSocket } from './connection-web.js';
import Cacher from './cacher.js';
import type { Server } from './schema.js';
import { type InferSelectModel } from 'drizzle-orm';
import { type InferSelectModel, sql } from 'drizzle-orm';
import Models from './models.js';
import process from 'node:process';
import * as pgtypes from './schema.js';
Expand Down Expand Up @@ -146,13 +146,170 @@ export default class Config {
} catch (err) {
console.log(`ok - no server config found: ${err instanceof Error ? err.message : String(err)}`);

server = await models.Server.generate({
name: 'Default Server',
url: 'ssl://localhost:8089',
api: 'https://localhost:8443'
// Create server with environment variables if available
const serverData: Record<string, unknown> = {
name: process.env.CLOUDTAK_Server_name || 'Default Server',
url: process.env.CLOUDTAK_Server_url || 'ssl://localhost:8089',
api: process.env.CLOUDTAK_Server_api || 'https://localhost:8443'
};

if (process.env.CLOUDTAK_Server_webtak) {
serverData.webtak = process.env.CLOUDTAK_Server_webtak;
}

// Handle auth certificates
if (process.env.CLOUDTAK_Server_auth_p12_secret_arn && process.env.CLOUDTAK_Server_auth_password) {
try {
const secrets = new SecretsManager.SecretsManagerClient({ region: process.env.AWS_REGION });
const secretValue = await secrets.send(new SecretsManager.GetSecretValueCommand({
SecretId: process.env.CLOUDTAK_Server_auth_p12_secret_arn
}));

if (secretValue.SecretBinary) {
const pem = (await import('pem')).default;
const p12Buffer = Buffer.from(secretValue.SecretBinary);

const certs = await new Promise<{ pemCertificate: string; pemKey: string }>((resolve, reject) => {
pem.readPkcs12(p12Buffer, { p12Password: process.env.CLOUDTAK_Server_auth_password }, (err: Error | null, result: { cert: string; key: string }) => {
if (err) {
reject(err);
} else {
resolve({ pemCertificate: result.cert, pemKey: result.key });
}
});
});

serverData.auth = {
cert: certs.pemCertificate,
key: certs.pemKey
};
console.error('ok - Extracted certificate and key from P12 binary secret');
}
} catch (err) {
console.error(`Error extracting P12 from binary secret: ${err instanceof Error ? err.message : String(err)}`);
}
} else if (process.env.CLOUDTAK_Server_auth_cert && process.env.CLOUDTAK_Server_auth_key) {
serverData.auth = {
cert: process.env.CLOUDTAK_Server_auth_cert,
key: process.env.CLOUDTAK_Server_auth_key
};
}

server = await models.Server.generate(serverData);
}

// Update server with environment variables
console.error(`ok - Initial server state: auth.cert=${!!server.auth?.cert}, auth.key=${!!server.auth?.key}, webtak=${!!server.webtak}`);
console.error(`ok - Environment variables: CLOUDTAK_Server_name=${process.env.CLOUDTAK_Server_name}, CLOUDTAK_Server_url=${process.env.CLOUDTAK_Server_url}, CLOUDTAK_Server_api=${process.env.CLOUDTAK_Server_api}, CLOUDTAK_Server_webtak=${process.env.CLOUDTAK_Server_webtak}`);
console.error(`ok - Auth env vars: CLOUDTAK_Server_auth_cert=${!!process.env.CLOUDTAK_Server_auth_cert}, CLOUDTAK_Server_auth_key=${!!process.env.CLOUDTAK_Server_auth_key}, CLOUDTAK_Server_auth_p12_secret_arn=${!!process.env.CLOUDTAK_Server_auth_p12_secret_arn}`);
console.error(`ok - Admin env vars: CLOUDTAK_ADMIN_USERNAME=${!!process.env.CLOUDTAK_ADMIN_USERNAME}, CLOUDTAK_ADMIN_PASSWORD=${!!process.env.CLOUDTAK_ADMIN_PASSWORD}`);

// Debug all CLOUDTAK environment variables
const cloudtakEnvs = Object.keys(process.env).filter(key => key.startsWith('CLOUDTAK_')).sort();
console.error(`ok - All CLOUDTAK env vars: ${cloudtakEnvs.join(', ')}`);

if (process.env.CLOUDTAK_Server_auth_p12_secret_arn) {
console.error(`ok - P12 secret ARN: ${process.env.CLOUDTAK_Server_auth_p12_secret_arn}`);
} else {
console.error('ok - CLOUDTAK_Server_auth_p12_secret_arn is undefined/empty');
}

if (process.env.CLOUDTAK_Server_auth_cert) {
console.error(`ok - Direct cert length: ${process.env.CLOUDTAK_Server_auth_cert.length}`);
}

if (process.env.CLOUDTAK_Server_auth_key) {
console.error(`ok - Direct key length: ${process.env.CLOUDTAK_Server_auth_key.length}`);
}

const serverEnvUpdates: Record<string, unknown> = {};
let hasServerUpdates = false;

if (process.env.CLOUDTAK_Server_name) {
serverEnvUpdates.name = process.env.CLOUDTAK_Server_name;
hasServerUpdates = true;
}
if (process.env.CLOUDTAK_Server_url) {
serverEnvUpdates.url = process.env.CLOUDTAK_Server_url;
hasServerUpdates = true;
}
if (process.env.CLOUDTAK_Server_api) {
serverEnvUpdates.api = process.env.CLOUDTAK_Server_api;
hasServerUpdates = true;
}
if (process.env.CLOUDTAK_Server_webtak) {
serverEnvUpdates.webtak = process.env.CLOUDTAK_Server_webtak;
hasServerUpdates = true;
}

// Handle auth certificates for existing server
console.error('ok - Updating server configuration from environment variables');

if (process.env.CLOUDTAK_Server_auth_p12_secret_arn && process.env.CLOUDTAK_Server_auth_password) {
console.error('ok - Processing P12 certificate from binary secret');
try {
const secrets = new SecretsManager.SecretsManagerClient({ region: process.env.AWS_REGION });
const secretValue = await secrets.send(new SecretsManager.GetSecretValueCommand({
SecretId: process.env.CLOUDTAK_Server_auth_p12_secret_arn
}));

if (secretValue.SecretBinary) {
const pem = (await import('pem')).default;
const p12Buffer = Buffer.from(secretValue.SecretBinary);

const certs = await new Promise<{ pemCertificate: string; pemKey: string }>((resolve, reject) => {
pem.readPkcs12(p12Buffer, { p12Password: process.env.CLOUDTAK_Server_auth_password }, (err: Error | null, result: { cert: string; key: string }) => {
if (err) {
reject(err);
} else {
resolve({ pemCertificate: result.cert, pemKey: result.key });
}
});
});

if (certs.pemCertificate && certs.pemKey) {
serverEnvUpdates.auth = {
...(server.auth || {}),
cert: certs.pemCertificate,
key: certs.pemKey
};
hasServerUpdates = true;
console.error('ok - Successfully extracted certificate and key from P12 binary secret');
} else {
console.error('ok - P12 conversion failed: missing certificate or key');
}
} else {
console.error('ok - No SecretBinary found in secret');
}
} catch (err) {
console.error(`ok - Error processing P12 binary secret: ${err instanceof Error ? err.message : String(err)}`);
}
} else if (process.env.CLOUDTAK_Server_auth_cert && process.env.CLOUDTAK_Server_auth_key) {
console.error('ok - Using direct certificate and key from environment variables');
serverEnvUpdates.auth = {
...(server.auth || {}),
cert: process.env.CLOUDTAK_Server_auth_cert,
key: process.env.CLOUDTAK_Server_auth_key
};
hasServerUpdates = true;
console.error(`ok - Direct auth configured: cert=${process.env.CLOUDTAK_Server_auth_cert.length} chars, key=${process.env.CLOUDTAK_Server_auth_key.length} chars`);
} else {
console.error('ok - No certificate environment variables found - server will run without client certificates');
}

if (hasServerUpdates) {
console.error(`ok - Updates to apply: ${JSON.stringify(Object.keys(serverEnvUpdates))}`);
server = await models.Server.commit(server.id, {
...serverEnvUpdates,
updated: sql`Now()`
});
console.error(`ok - Server updated: auth.cert=${!!server.auth?.cert}, auth.key=${!!server.auth?.key}, webtak=${!!server.webtak}`);
} else {
console.error('ok - No server updates needed from environment variables');
}

console.error(`ok - Final server state before Config creation: auth.cert=${!!server.auth?.cert}, auth.key=${!!server.auth?.key}, webtak=${!!server.webtak}`);

const config = new Config({
silent: (args.silent || false),
noevents: (args.noevents || false),
Expand All @@ -163,8 +320,10 @@ export default class Config {
server, SigningSecret, MediaSecret, API_URL, DynamoDB, Bucket, pg, models, PMTILES_URL
});

console.error(`ok - Config created with server: auth.cert=${!!config.server.auth?.cert}, auth.key=${!!config.server.auth?.key}, webtak=${!!config.server.webtak}`);

if (!config.silent) {
console.error('ok - set env AWS_REGION: us-east-1');
console.error(`ok - set env AWS_REGION: ${process.env.AWS_REGION}`);
console.log(`ok - PMTiles: ${config.PMTILES_URL}`);
console.error(`ok - StackName: ${config.StackName}`);
}
Expand All @@ -177,6 +336,24 @@ export default class Config {
if (process.env.SubnetPublicB) config.SubnetPublicB = process.env.SubnetPublicB;
if (process.env.MediaSecurityGroup) config.MediaSecurityGroup = process.env.MediaSecurityGroup;

// Ensure admin user has admin permissions if credentials provided
if (process.env.CLOUDTAK_ADMIN_USERNAME && process.env.CLOUDTAK_ADMIN_PASSWORD) {
try {
console.error('ok - Ensuring admin user has admin permissions');

// Create admin user directly in database with admin permissions
await config.models.Profile.generate({
username: process.env.CLOUDTAK_ADMIN_USERNAME,
auth: { password: process.env.CLOUDTAK_ADMIN_PASSWORD },
system_admin: true
}, { upsert: GenerateUpsert.UPDATE });

console.error('ok - Admin user ensured with admin permissions');
} catch (err) {
console.error(`Error ensuring admin user: ${err instanceof Error ? err.message : String(err)}`);
}
}

for (const envkey in process.env) {
if (!envkey.startsWith('CLOUDTAK')) continue;

Expand Down
4 changes: 2 additions & 2 deletions api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

130 changes: 130 additions & 0 deletions api/test/config.srv.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import test from 'tape';
import Flight from './flight.js';
import sinon from 'sinon';

const flight = new Flight();

Expand Down Expand Up @@ -168,4 +169,133 @@ test('GET api/config/map', async (t) => {
t.end();
});

// Server Environment Variable Integration Tests
test('Server Env: CLOUDTAK_Server_name updates database', async (t) => {
const originalEnv = process.env.CLOUDTAK_Server_name;
process.env.CLOUDTAK_Server_name = 'Env Test Server';

try {
const initialServer = await flight.config.models.Server.from(1);

// Simulate server env update
const updatedServer = await flight.config.models.Server.commit(initialServer.id, {
name: process.env.CLOUDTAK_Server_name
});

t.equal(updatedServer.name, 'Env Test Server');
} catch (err) {
t.error(err, 'no error');
}

process.env.CLOUDTAK_Server_name = originalEnv;
t.end();
});

test('Server Env: CLOUDTAK_Server_url updates database', async (t) => {
const originalEnv = process.env.CLOUDTAK_Server_url;
process.env.CLOUDTAK_Server_url = 'ssl://env.test.com:8089';

try {
const initialServer = await flight.config.models.Server.from(1);

const updatedServer = await flight.config.models.Server.commit(initialServer.id, {
url: process.env.CLOUDTAK_Server_url
});

t.equal(updatedServer.url, 'ssl://env.test.com:8089');
} catch (err) {
t.error(err, 'no error');
}

process.env.CLOUDTAK_Server_url = originalEnv;
t.end();
});

test('Server Env: auth object updates database', async (t) => {
const originalCert = process.env.CLOUDTAK_Server_auth_cert;
const originalKey = process.env.CLOUDTAK_Server_auth_key;

process.env.CLOUDTAK_Server_auth_cert = 'test-cert-data';
process.env.CLOUDTAK_Server_auth_key = 'test-key-data';

try {
const initialServer = await flight.config.models.Server.from(1);

const updatedServer = await flight.config.models.Server.commit(initialServer.id, {
auth: {
cert: process.env.CLOUDTAK_Server_auth_cert,
key: process.env.CLOUDTAK_Server_auth_key
}
});

t.deepEqual(updatedServer.auth, {
cert: 'test-cert-data',
key: 'test-key-data'
});
} catch (err) {
t.error(err, 'no error');
}

process.env.CLOUDTAK_Server_auth_cert = originalCert;
process.env.CLOUDTAK_Server_auth_key = originalKey;
t.end();
});

test('Server Env: multiple fields update database', async (t) => {
const originalVars = {
name: process.env.CLOUDTAK_Server_name,
api: process.env.CLOUDTAK_Server_api,
webtak: process.env.CLOUDTAK_Server_webtak
};

process.env.CLOUDTAK_Server_name = 'Multi Field Server';
process.env.CLOUDTAK_Server_api = 'https://multi.test.com:8443';
process.env.CLOUDTAK_Server_webtak = 'http://multi.test.com:8444';

try {
const initialServer = await flight.config.models.Server.from(1);

const updatedServer = await flight.config.models.Server.commit(initialServer.id, {
name: process.env.CLOUDTAK_Server_name,
api: process.env.CLOUDTAK_Server_api,
webtak: process.env.CLOUDTAK_Server_webtak
});

t.equal(updatedServer.name, 'Multi Field Server');
t.equal(updatedServer.api, 'https://multi.test.com:8443');
t.equal(updatedServer.webtak, 'http://multi.test.com:8444');
} catch (err) {
t.error(err, 'no error');
}

// Restore environment
Object.keys(originalVars).forEach(key => {
const envVar = `CLOUDTAK_Server_${key}`;
if (originalVars[key] !== undefined) {
process.env[envVar] = originalVars[key];
} else {
delete process.env[envVar];
}
});

t.end();
});

test('Server Env: schema validation with invalid field', async (t) => {
try {
const initialServer = await flight.config.models.Server.from(1);

// This should fail if schema validation is working
await flight.config.models.Server.commit(initialServer.id, {
invalidField: 'should-fail'
});

t.fail('Should have thrown error for invalid field');
} catch {
t.pass('Correctly rejected invalid field');
}

t.end();
});

flight.landing();
2 changes: 1 addition & 1 deletion api/test/flight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ export default class Flight {

Object.assign(this.config, custom);

this.config.models.Server.generate({
await this.config.models.Server.generate({
name: 'Test Runner',
url: 'ssl://localhost',
auth: {
Expand Down
Loading