Skip to content
Closed
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
81 changes: 69 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,28 +26,85 @@ At the moment, PubPub isn't particularly well setup for outside contributors. Ho

User-facing documentation is a work in progress, and can be found at https://help.pubpub.org. If you're interested in helping contribute to documentation, we'd love to hear from you. Please send a note to [hello@pubpub.org](mailto:hello@pubpub.org?subject=Documentation%20Contribution) introducing yourself and describing how you'd like to contribute.

## To Install

```
## Local Development

### Prerequisites

- **Docker & Docker Compose**
- **pnpm** (see `packageManager` in `package.json`; `corepack enable` to activate)
- **SOPS + age** for decrypting environment secrets:
```
brew install sops age
```
- **Age key** — ask a team member for the private key, then place it at:
```
~/.config/sops/age/keys.txt
```
- **pandoc + poppler** (macOS):
```
brew install pandoc poppler
```
(See `Aptfile` for equivalent Debian packages)

### Quick Start

PubPub requires [KF Auth](https://github.com/knowledgefutures/kf-auth-v3) for authentication. Start it first:

```bash
# Terminal 1: Start KF Auth
cd ../kf-auth-v3
pnpm install
pnpm dev
```

```bash
# Terminal 2: Start PubPub
pnpm install
pnpm dev
```

To run locally on a Mac, use [Homebrew](https://brew.sh/) to install a handful of dependencies:
On first run, `pnpm dev` will:
1. Auto-decrypt `infra/.env.local.enc` into `infra/.env.local` (requires age key)
2. Start PostgreSQL, RabbitMQ, app server, and worker via Docker
3. Seed a demo community if the database is empty

```
brew install pandoc poppler
```
Navigate to `http://localhost:9876`

(See `Aptfile` for a list of equivalent Debian packages to install)
### Default Credentials

## To Run Dev Mode
Sign in at http://localhost:9876 using the KF Auth seed admin account:
- **Email:** `admin@kfauth.org`
- **Password:** `admin123`

```
pnpm dev
### Useful URLs

| URL | Service |
|-----|---------|
| http://localhost:9876 | PubPub |
| http://localhost:3000 | KF Auth (sign-in) |
| http://localhost:3001 | KF Auth account dashboard |
| http://localhost:9999 | Inbucket (email inbox for dev) |

### Secrets

Environment secrets are encrypted with SOPS + age. Decryption happens automatically
on `pnpm dev`, but you can also run manually:

```bash
pnpm secrets:decrypt:local # infra/.env.local.enc → infra/.env.local
pnpm secrets:encrypt:local # infra/.env.local → infra/.env.local.enc (after changes)
```

Navigate to `localhost:9876`
### Troubleshooting

**`env file infra/.env.local not found`**
Auto-decrypt may have failed. Ensure your age key is at `~/.config/sops/age/keys.txt`, then run `pnpm secrets:decrypt:local`.

**Every page returns 404**
The demo community may not have been seeded. Check Docker logs for `[seed]` messages. To reset from scratch: `docker compose -f infra/docker-compose.dev.yml down -v && pnpm dev`

**Cannot log in / redirect loop**
Make sure KF Auth is running on port 3000. PubPub expects the OIDC issuer at `http://localhost:3000`.

## Fonts

Expand Down
106 changes: 53 additions & 53 deletions infra/.env.dev.enc

Large diffs are not rendered by default.

109 changes: 55 additions & 54 deletions infra/.env.local.enc

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions infra/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,31 @@ set -euo pipefail

cd "$(dirname "$0")/.."

# ── Auto-decrypt local env if needed ──────────────────────────────────
ENV_FILE="infra/.env.local"
ENC_FILE="infra/.env.local.enc"

if [[ ! -f "$ENV_FILE" ]]; then
if [[ -f "$ENC_FILE" ]]; then
echo "Decrypting $ENC_FILE → $ENV_FILE ..."
if ! sops -d --input-type dotenv --output-type dotenv \
--output "$ENV_FILE" "$ENC_FILE" 2>/dev/null; then
echo ""
echo " Decryption failed. Make sure:"
echo " 1. sops and age are installed (brew install sops age)"
echo " 2. Your age key is at ~/.config/sops/age/keys.txt"
echo " 3. Your age public key is listed in infra/.sops.yaml"
echo ""
exit 1
fi
echo "Decrypted successfully."
else
echo "Error: $ENV_FILE not found and no $ENC_FILE to decrypt."
echo "Run: pnpm secrets:decrypt:local"
exit 1
fi
fi

MODE="${1:-dev}"

case "$MODE" in
Expand Down
4 changes: 1 addition & 3 deletions infra/docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ services:
PORT: 3000
DATABASE_URL: postgres://appuser:apppassword@db:5432/appdb
CLOUDAMQP_URL: amqp://appuser:apppassword@rabbitmq:5672/appvhost
PUBPUB_LOCAL_COMMUNITY: demo
depends_on:
- db
- rabbitmq
Expand Down Expand Up @@ -86,8 +87,6 @@ services:
- "${DB_PORT:-5439}:5432"
networks: [appnet]



# cron:
# build:
# context: ..
Expand All @@ -113,4 +112,3 @@ networks:
volumes:
pgdata:
rabbitmqdata:

2 changes: 2 additions & 0 deletions server/kf/oidc.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const OIDC_CLIENT_ID = process.env.OIDC_CLIENT_ID ?? 'kf_pubpub';
const OIDC_CLIENT_SECRET = process.env.OIDC_CLIENT_SECRET ?? '';

const OIDC_ORGS_CLAIM = process.env.OIDC_ORGS_CLAIM ?? 'https://knowledgefutures.org/orgs';
const OIDC_ROLE_CLAIM = process.env.OIDC_ROLE_CLAIM ?? 'https://knowledgefutures.org/role';

const APP_URL = process.env.APP_URL ?? 'http://localhost:9876';
const REDIRECT_URI = `${APP_URL}/auth/callback`;
Expand Down Expand Up @@ -416,6 +417,7 @@ export {
OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET,
OIDC_ORGS_CLAIM,
OIDC_ROLE_CLAIM,
OIDC_ACCOUNT_URL,
APP_URL,
REDIRECT_URI,
Expand Down
22 changes: 18 additions & 4 deletions server/kf/provisionLocalUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ import { Op } from 'sequelize';
import { User } from 'server/models';
import { slugifyString } from 'utils/strings';

import { OIDC_ROLE_CLAIM } from './oidc.server';

type UserInfoFields = Partial<
Pick<OIDCUserInfo, 'name' | 'email' | 'picture' | 'given_name' | 'family_name'> & {
[key: string]: unknown;
}
>;

/**
* Look up the local PubPub `User` row that corresponds to a kf-auth subject,
* auto-creating it from kf-auth userinfo on first contact.
Expand All @@ -18,12 +26,17 @@ import { slugifyString } from 'utils/strings';
*/
export async function provisionLocalUser(
kfUserId: string,
userInfo: Partial<
Pick<OIDCUserInfo, 'name' | 'email' | 'picture' | 'given_name' | 'family_name'>
>,
userInfo: UserInfoFields,
): Promise<InstanceType<typeof User>> {
const isSuperAdmin = userInfo[OIDC_ROLE_CLAIM] === 'admin';

const existing = await User.findOne({ where: { id: kfUserId } });
if (existing) return existing;
if (existing) {
if (existing.isSuperAdmin !== isSuperAdmin) {
await existing.update({ isSuperAdmin });
}
return existing;
}

const firstName = (userInfo.given_name || userInfo.name || 'New').trim();
const lastName = (userInfo.family_name || 'User').trim();
Expand Down Expand Up @@ -57,6 +70,7 @@ export async function provisionLocalUser(
initials,
email,
avatar: userInfo.picture || null,
isSuperAdmin,
hash: '',
salt: '',
} as any);
Expand Down
107 changes: 107 additions & 0 deletions server/seed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import crypto from 'crypto';

import { Community, FeatureFlag, FeatureFlagCommunity, Page, SpamTag } from './models';

const DEMO_SUBDOMAIN = 'demo';

export async function seedDevData() {
if (process.env.NODE_ENV === 'production') return;

const existing = await Community.findOne({
where: { subdomain: DEMO_SUBDOMAIN },
attributes: ['id'],
});
if (existing) return;

console.log('[seed] Creating demo community...');

const communityId = crypto.randomUUID();
const homePageId = crypto.randomUUID();

try {
const customFeatureFlag = await FeatureFlag.create({
id: crypto.randomUUID(),
name: 'customScripts',
});

const spamTag = await SpamTag.create({
id: crypto.randomUUID(),
status: 'confirmed-not-spam',
fields: {},
spamScore: 0,

spamScoreComputedAt: new Date(),
spamScoreVersion: 1,
});
await Community.create(
{
id: communityId,
subdomain: DEMO_SUBDOMAIN,
title: 'Demo Community',
description: 'Local development community',
heroTitle: 'Demo Community',
heroText: 'Welcome to your local PubPub development environment.',
accentColorLight: '#ffffff',
accentColorDark: '#112233',
navigation: [{ type: 'page', id: homePageId }],
hideCreatePubButton: false,
spamTagId: spamTag.id,
},
{ hooks: false },
);
await FeatureFlagCommunity.create({
id: crypto.randomUUID(),
featureFlagId: customFeatureFlag.id,
communityId,
enabled: true,
});

await Page.create({
id: homePageId,
title: 'Home',
slug: '',
communityId,
isPublic: true,
layout: [
{
id: crypto.randomUUID().slice(0, 8),
type: 'text',
content: {
text: {
type: 'doc',
attrs: { meta: {} },
content: [
{
type: 'heading',
attrs: { level: 1, fixedId: '', id: 'welcome' },
content: [{ type: 'text', text: 'Welcome to PubPub Dev' }],
},
{
type: 'paragraph',
attrs: { class: null },
content: [
{
type: 'text',
text: 'This community was automatically created for local development. Sign in with your KF Auth account to get started.',
},
],
},
],
},
align: 'left',
title: '',
width: 'wide',
},
},
],
viewHash: crypto.randomUUID().slice(0, 8),
} as any);

console.log('[seed] Demo community created (subdomain: demo)');
} catch (err: any) {
if (err?.name === 'SequelizeUniqueConstraintError') {
return;
}
throw err;
}
}
7 changes: 7 additions & 0 deletions server/sequelize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,5 +146,12 @@ export const sequelizeSyncPromise: Promise<void> =
// take several minutes and would delay deploys.
const { createSummaryViews } = await import('server/analytics/summaryViews');
await createSummaryViews();

if (process.env.NODE_ENV !== 'production') {
const { seedDevData } = await import('server/seed');
await seedDevData().catch((err) =>
console.error('[seed] Failed to seed dev data:', err),
);
}
})()
: Promise.resolve();
Loading