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
10 changes: 5 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
VITE_SERVER_URL=http://localhost:9901 # Make sure this matches the PORT value below (in development)
FRONTEND_URL=http://localhost:5173 # Make sure this matches the vite.config.ts

POSTGRES_DB_URL=postgresql://
POSTGRES_DB_URL_FLIGHTS=postgresql://
POSTGRES_DB_URL_CHATS=postgresql://
POSTGRES_DB_URL=postgresql://pfcontrol:YOUR_POSTGRES_PASSWORD_HERE@localhost:5432/pfcontrol
POSTGRES_DB_URL_FLIGHTS=postgresql://pfcontrol:YOUR_POSTGRES_PASSWORD_HERE@localhost:5432/pfcontrol_flights
POSTGRES_DB_URL_CHATS=postgresql://pfcontrol:YOUR_POSTGRES_PASSWORD_HERE@localhost:5432/pfcontrol_chats
POSTGRES_DB_PW=YOUR_POSTGRES_PASSWORD_HERE
DB_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE # Must be 128 Characters Long
REDIS_URL=redis://
DB_ENCRYPTION_KEY=YOUR_ENCRYPTION_KEY_HERE # Must be 128 Characters Long - use this in terminal: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
REDIS_URL=redis://localhost:6379

DISCORD_CLIENT_ID=YOUR_CLIENT_ID_HERE # Get it from https://discord.com/developers/applications
DISCORD_CLIENT_SECRET=YOUR_CLIENT_SECRET_HERE # Get it from https://discord.com/developers/applications
Expand Down
53 changes: 41 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,56 @@ If you just want to try or demo PFControl:

## Development — Local setup

The following steps get the project running on your machine for development and testing.
Getting started with PFControl development is straightforward. We've set up Docker Compose to handle PostgreSQL and Redis for you, so you don't need to manually configure databases.

1. Install dependencies
### Setup instructions

```
npm install
```
1. **Install Docker Desktop** (if you don't have it)
Download from [docker.com](https://www.docker.com/products/docker-desktop) and open it. Make sure it fully starts up.

2. Create an environment file
Copy the example and update environment variables into `.env.development`.
2. **Clone the repository**
```bash
git clone https://github.com/PFConnect/pfcontrol-2.git
cd pfcontrol-2
```

> **Note:** For full functionality, you must set up PostgreSQL and Redis and provide the correct connection URLs in your `.env.development` file.
> If you are unable to set up these services locally, you can still run the frontend, but backend features will be limited or unavailable.
> If you need help or require development environment variables, join our [Discord server](https://pfconnect.online/discord), create a ticket, and ask for assistance.
3. **Start PostgreSQL and Redis**
```bash
docker-compose -f docker-compose.dev.yml up -d
```
This starts local PostgreSQL and Redis containers in the background. First-time setup downloads the images and takes about 30 seconds.

3. Start the development environment
4. **Set up your environment file**
```bash
cp .env.example .env.development
```
This creates a `.env.development` file with localhost connection strings that point to the Docker containers.

5. **Install dependencies and start the dev server**
```bash
npm install
npm run dev
```

Frontend will be available at http://localhost:5173 and Backend API at http://localhost:9901 by default.
That's it! The frontend will be at [http://localhost:5173](http://localhost:5173) and the backend API at [http://localhost:9901](http://localhost:9901).

**When you're done**, stop the databases with:
```bash
docker-compose -f docker-compose.dev.yml down
```

To reset your local database (fresh start), run:
```bash
docker-compose -f docker-compose.dev.yml down -v
docker-compose -f docker-compose.dev.yml up -d
```

### Troubleshooting

- **"Cannot connect to the Docker daemon"**: Make sure Docker Desktop is running. Open the Docker Desktop application before running docker-compose commands
- **"Cannot connect to PostgreSQL"**: Make sure Docker Compose is running (`docker ps` should show postgres and redis containers)
- **"Port 5432 already in use"**: You might have PostgreSQL installed locally. Either stop your local PostgreSQL or change the port mapping in `docker-compose.dev.yml`
- **Need help?** Join our [Discord server](https://pfconnect.online/discord), create a ticket, and we'll help you out

## Project structure

Expand Down
45 changes: 45 additions & 0 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,42 @@
version: '3.8'

services:
postgres:
image: postgres:16-alpine
container_name: pfcontrol-postgres-dev
ports:
- '5432:5432'
environment:
POSTGRES_USER: pfcontrol
POSTGRES_PASSWORD: pfcontrol
POSTGRES_DB: pfcontrol
volumes:
- postgres_data:/var/lib/postgresql/data
restart: unless-stopped
networks:
- pfcontrol-network
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U pfcontrol']
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
container_name: pfcontrol-redis-dev
ports:
- '6379:6379'
volumes:
- redis_data:/data
restart: unless-stopped
networks:
- pfcontrol-network
healthcheck:
test: ['CMD', 'redis-cli', 'ping']
interval: 10s
timeout: 3s
retries: 5

pfcontrol-dev:
build:
context: .
Expand All @@ -18,6 +54,15 @@ services:
restart: unless-stopped
networks:
- pfcontrol-network
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy

volumes:
postgres_data:
redis_data:

networks:
pfcontrol-network:
Expand Down
76 changes: 65 additions & 11 deletions server/db/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,66 @@ import type { MainDatabase } from './types/connection/MainDatabase';
import type { FlightsDatabase } from './types/connection/FlightsDatabase';
import type { ChatsDatabase } from './types/connection/ChatsDatabase';

// create databases if they don't exist
async function ensureDatabasesExist() {
const mainDbUrl = process.env.POSTGRES_DB_URL;
if (!mainDbUrl) {
throw new Error('POSTGRES_DB_URL is not defined');
}

const url = new URL(mainDbUrl);
const baseConnectionString = `postgresql://${url.username}:${url.password}@${url.host}/postgres`;

const isLocalhost = url.hostname === 'localhost' || url.hostname === '127.0.0.1';

const client = new pg.Client({
connectionString: baseConnectionString,
ssl: isLocalhost ? false : { rejectUnauthorized: false },
});

try {
await client.connect();

const flightsDbName = 'pfcontrol_flights';
const flightsResult = await client.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[flightsDbName]
);
if (flightsResult.rows.length === 0) {
await client.query(`CREATE DATABASE ${flightsDbName}`);
console.log(`[Database] Created database: ${flightsDbName}`);
}

const chatsDbName = 'pfcontrol_chats';
const chatsResult = await client.query(
`SELECT 1 FROM pg_database WHERE datname = $1`,
[chatsDbName]
);
if (chatsResult.rows.length === 0) {
await client.query(`CREATE DATABASE ${chatsDbName}`);
console.log(`[Database] Created database: ${chatsDbName}`);
}
} catch (error) {
console.error('[Database] Error ensuring databases exist:', error);
throw error;
} finally {
await client.end();
}
}

await ensureDatabasesExist();

function getSSLConfig(connectionString: string) {
const url = new URL(connectionString);
const isLocalhost = url.hostname === 'localhost' || url.hostname === '127.0.0.1';
return isLocalhost ? false : { rejectUnauthorized: false };
}

export const mainDb = new Kysely<MainDatabase>({
dialect: new PostgresDialect({
pool: new pg.Pool({
connectionString: process.env.POSTGRES_DB_URL,
ssl: { rejectUnauthorized: false },
ssl: getSSLConfig(process.env.POSTGRES_DB_URL as string),
}),
}),
});
Expand All @@ -28,7 +83,7 @@ export const flightsDb = new Kysely<FlightsDatabase>({
dialect: new PostgresDialect({
pool: new pg.Pool({
connectionString: process.env.POSTGRES_DB_URL_FLIGHTS,
ssl: { rejectUnauthorized: false },
ssl: getSSLConfig(process.env.POSTGRES_DB_URL_FLIGHTS as string),
}),
}),
});
Expand All @@ -37,7 +92,7 @@ export const chatsDb = new Kysely<ChatsDatabase>({
dialect: new PostgresDialect({
pool: new pg.Pool({
connectionString: process.env.POSTGRES_DB_URL_CHATS,
ssl: { rejectUnauthorized: false },
ssl: getSSLConfig(process.env.POSTGRES_DB_URL_CHATS as string),
}),
}),
});
Expand All @@ -55,12 +110,11 @@ redisConnection.on('connect', () => {
console.log('[Redis] Connected successfully');
});

createMainTables().catch((err) => {
console.error('Failed to create main tables:', err);
process.exit(1);
});

createGlobalChatTable().catch((err) => {
console.error('Failed to create global chat table:', err);
try {
await createMainTables();
await createGlobalChatTable();
console.log('[Database] Tables initialized successfully');
} catch (err) {
console.error('Failed to create tables:', err);
process.exit(1);
});
}
15 changes: 9 additions & 6 deletions server/db/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ export async function createMainTables() {
await mainDb.schema
.createTable('app_settings')
.ifNotExists()
.addColumn('key', 'varchar(255)', (col) => col.primaryKey())
.addColumn('value', 'text')
.addColumn('id', 'integer', (col) => col.primaryKey())
.addColumn('version', 'varchar(50)', (col) => col.notNull())
.addColumn('updated_at', 'timestamp', (col) => col.notNull())
.addColumn('updated_by', 'varchar(255)', (col) => col.notNull())
.execute();

// users (assuming based on typical UsersTable interface)
Expand All @@ -27,6 +29,7 @@ export async function createMainTables() {
.createTable('sessions')
.ifNotExists()
.addColumn('id', 'varchar(255)', (col) => col.primaryKey())
.addColumn('session_id', 'varchar(255)', (col) => col.notNull())
.addColumn('user_id', 'varchar(255)', (col) =>
col.references('users.id').onDelete('cascade')
)
Expand Down Expand Up @@ -89,6 +92,7 @@ export async function createMainTables() {
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('title', 'varchar(255)', (col) => col.notNull())
.addColumn('message', 'text', (col) => col.notNull())
.addColumn('show', 'boolean', (col) => col.defaultTo(true))
.addColumn('created_at', 'timestamp', (col) => col.defaultTo('now()'))
.execute();

Expand Down Expand Up @@ -122,10 +126,9 @@ export async function createMainTables() {
await mainDb.schema
.createTable('tester_settings')
.ifNotExists()
.addColumn('tester_id', 'integer', (col) =>
col.references('testers.id').onDelete('cascade').primaryKey()
)
.addColumn('settings', 'jsonb')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('setting_key', 'varchar(255)', (col) => col.unique().notNull())
.addColumn('setting_value', 'boolean', (col) => col.notNull())
.execute();

// daily_statistics
Expand Down
Loading