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
36 changes: 36 additions & 0 deletions .env.prod.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Production Environment Configuration
# This file serves as a template for GitHub Secrets configuration
#
# IMPORTANT: Secrets are injected directly into Docker containers via environment
# variables. No .env files are created on the host system for security.

# Domain Configuration
DOMAIN=

# Container Registry Configuration
FRONTEND_IMAGE=
BACKEND_IMAGE=

# Database Configuration (External PostgreSQL)
PGUSER=
PGPASSWORD=
PGHOST=
PGDATABASE=
PGPORT=

# Security Keys (Store as GitHub Secrets)
ENCRYPTION_KEY=
JWT_SECRET=

# RSKY Configuration
RSKY_FEEDGEN=
RSKY_API_KEY=your_rsky_api_key

# Fixed Configuration (automatically set in deployment)
PORT=
CLIENT_URL=
BASE_URL=
BSKY_BASE_API_URL=https://api.bsky.app
MUTE_LIST_URI=
MUTE_LIST_ADMIN_DID=
RECONCILIATION_INTERVAL_MS=
15 changes: 15 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
PORT=3000
NODE_ENV=test
PGHOST=db
PGUSER=postgres
PGPASSWORD=testpassword
PGDATABASE=safeskiesdb
PGPORT=5432
JWT_SECRET=test-jwt-secret
CLIENT_URL=http://localhost:3000
BASE_URL=http://localhost:3000
MUTE_LIST_URI=test-uri
MUTE_LIST_ADMIN_DID=test-did
BSKY_BASE_API_URL=https://bsky.social
RSKY_FEEDGEN=http://localhost:8080
RSKY_API_KEY=test-key
27 changes: 27 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI - Validate Changes

on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
validate:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Run type checking
run: npm run type-check
49 changes: 49 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Deploy to Production

on:
push:
branches: [main]

env:
IMAGE_NAME: safe-skies-api

jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Install doctl
uses: digitalocean/action-doctl@v2
with:
token: ${{ secrets.DO_REGISTRY_TOKEN }}

- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ secrets.DO_REGISTRY_URL }}
tags: |
type=sha,prefix={{branch}}-
type=raw,value=latest

- name: Log in to DigitalOcean Container Registry with short-lived credentials
run: doctl registry login --expiry-seconds 1200

- name: Build and push image to DigitalOcean Container Registry
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
9 changes: 7 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@ RUN chown -R node:node /usr/src/app

USER node
COPY --chown=node:node . .
RUN npm run build
RUN npm run build && npm run build:knexfile

FROM base AS prodrunner
COPY docker-entrypoint.sh ./docker-entrypoint.sh
RUN chmod +x ./docker-entrypoint.sh && sed -i 's/\r$//' ./docker-entrypoint.sh && chown node:node ./docker-entrypoint.sh
USER node
COPY --from=prodbuilder --chown=node:node /usr/src/app/node_modules ./node_modules
COPY --from=prodbuilder --chown=node:node /usr/src/app/dist ./dist
CMD node dist/src/server.js
COPY --from=prodbuilder --chown=node:node /usr/src/app/migrations ./migrations
COPY --from=prodbuilder --chown=node:node /usr/src/app/knexfile.js ./knexfile.js
COPY --from=prodbuilder --chown=node:node /usr/src/app/package.json ./package.json
ENTRYPOINT ["./docker-entrypoint.sh"]
14 changes: 14 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
services:
backend:
image: safe-skies-api:test
env_file: .env.test
db:
environment:
- POSTGRES_DB=safeskiesdb
- POSTGRES_PASSWORD=testpassword
healthcheck:
test: ["CMD", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
8 changes: 8 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh
set -e

echo "Running database migrations..."
npm run migrate:up

echo "Starting server..."
exec node dist/src/server.js
20 changes: 20 additions & 0 deletions migrations/20251028012216_oauth_keys_table.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Knex } from "knex";

export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("oauth_keys", (table) => {
table.text("key_id").primary();
table.text("private_key").notNullable();
table.text("public_key").notNullable();
table.text("algorithm").notNullable().defaultTo("ES256");
table.boolean("is_active").notNullable().defaultTo(true);
table.timestamp("created_at").defaultTo(knex.fn.now());

table.index("is_active", "idx_oauth_keys_active");
table.index("created_at", "idx_oauth_keys_created_at");
});
}

export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTableIfExists("oauth_keys");
}

7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
"type-check": "tsc --noEmit",
"format": "biome format . --write",
"prepare": "husky install",
"migrate:create": "knex migrate:make",
"migrate:up": "knex migrate:latest",
"migrate:down": "knex migrate:down"
"migrate:create": "ts-node node_modules/.bin/knex migrate:make",
"migrate:up": "ts-node node_modules/.bin/knex migrate:latest",
"migrate:down": "ts-node node_modules/.bin/knex migrate:down",
"build:knexfile": "tsc knexfile.ts --outDir . --esModuleInterop --module commonjs --target ES2020 --skipLibCheck"
},
"keywords": [],
"author": "",
Expand Down
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ app.use((req, res, next) => {
next();
});

app.use('/auth/admin', adminAuthRouter);
app.use('/auth', authRouter);
app.use('/admin/auth', adminAuthRouter);
app.use('/oauth', clientMetadataRouter);
app.use('/api/feeds', feedsRouter);
app.use('/api', profileRouter);
Expand Down
2 changes: 1 addition & 1 deletion src/controllers/admin-auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export const adminStatus = async (req: Request, res: Response): Promise<void> =>
res.json({
isAuthenticated,
adminDid,
loginUrl: isAuthenticated ? null : `${process.env.BASE_URL}/admin/auth/signin?handle=${adminDid}`
loginUrl: isAuthenticated ? null : `${process.env.BASE_URL}/auth/admin/signin?handle=${adminDid}`
});
} catch (err) {
console.error("Error checking admin status:", err);
Expand Down
Loading