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
1 change: 1 addition & 0 deletions .node-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
20.19.6
13 changes: 8 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
FROM node:18.15 as builder
RUN apt-get -qy update && apt-get install -qy openssl
FROM node:20-alpine3.18 AS builder
RUN apk add --no-cache openssl postgresql-client
WORKDIR /app
COPY ./package.json ./yarn.lock /app/

COPY ./ /app
RUN yarn install --frozen-lockfile

RUN yarn build
RUN mkdir -p /app/dist

FROM node:18.15-slim as runtime
RUN apt-get -qy update && apt-get install -qy openssl
FROM node:20-alpine3.18 AS runtime
RUN apk add --no-cache openssl postgresql-client

WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder "/app/dist/" "/app/dist/"
COPY --from=builder "/app/node_modules/" "/app/node_modules/"
COPY --from=builder "/app/package.json" "/app/package.json"
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh

CMD ["yarn", "start"]
CMD ["/docker-entrypoint.sh"]
57 changes: 55 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,62 @@ Required Environment Variables:
- AUTH0_DOMAIN
- AUTH0_CLIENT_ID
- AUTH0_CLIENT_SECRET
- ROLE_CODES
- AUTH0_HOOK_SHARED_SECRET
- DISCORD_BOT_TOKEN
- AUTH_SECRET
- STRIPE_SECRET_KEY
- UPLOADER_BASE

Optional Environment Variables:
- PORT
- PORT
- UPLOADER_SECRET

## Local Development Utilities

Use Node 20 for local development (`.nvmrc` and `.node-version` are included).

### Configure environment variables

1. Copy `.env.template` to `.env`.
2. Fill in real secrets in `.env`.
3. Keep `.env` uncommitted (it is gitignored).

### Start local services

```bash
docker compose up -d
```

### Seed local dummy fixture data

```bash
yarn seed-dummy
```

This writes `scripts/.seed-dummy.json` with event/mentor/student/project fixture data for local testing workflows.

### Generate an auth token

```bash
yarn generate-token -- event-test-2025 admin
```

Roles supported: `admin`, `manager`, `mentor`, `student`.

### Test GraphQL queries

```bash
API_KEY=<token-from-generate-token> yarn test-queries
```

Fail CI when Auth0-backed checks are skipped:

```bash
API_KEY=<token-from-generate-token> yarn test-queries:strict
```

You can also enable strict mode via environment variable:

```bash
TEST_QUERIES_STRICT=1 API_KEY=<token> yarn test-queries
```
65 changes: 65 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
services:
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: account_gql
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- "5434:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d account_gql"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data

elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.17.9
environment:
discovery.type: single-node
xpack.security.enabled: "false"
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
ports:
- "9200:9200"
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:9200 >/dev/null || exit 1"]
interval: 20s
timeout: 10s
retries: 10
volumes:
- elasticsearch_data:/usr/share/elasticsearch/data

app:
build:
context: .
dockerfile: Dockerfile
depends_on:
postgres:
condition: service_healthy
elasticsearch:
condition: service_healthy
environment:
NODE_ENV: ${NODE_ENV:-development}
PORT: ${PORT:-5000}
AUTH_SECRET: ${AUTH_SECRET:-local-dev-secret}
AUTH0_DOMAIN: ${AUTH0_DOMAIN:-local-dev.auth0.invalid}
AUTH0_CLIENT_ID: ${AUTH0_CLIENT_ID:-local-client-id}
AUTH0_CLIENT_SECRET: ${AUTH0_CLIENT_SECRET:-local-client-secret}
AUTH0_HOOK_SHARED_SECRET: ${AUTH0_HOOK_SHARED_SECRET:-local-user-secret}
DISCORD_BOT_TOKEN: ${DISCORD_BOT_TOKEN:-local-discord-token}
STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY:-sk_test_local}
UPLOADER_BASE: ${UPLOADER_BASE:-http://localhost:3001}
UPLOADER_SECRET: ${UPLOADER_SECRET:-local-uploader-secret}
ports:
- "5000:5000"
healthcheck:
test: ["CMD-SHELL", "node -e \"const net=require('net');const s=net.connect(5000,'127.0.0.1');s.on('connect',()=>{s.end();process.exit(0)});s.on('error',()=>process.exit(1));setTimeout(()=>process.exit(1),2000);\""]
interval: 20s
timeout: 10s
retries: 10

volumes:
postgres_data:
elasticsearch_data:
4 changes: 4 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
set -e

node dist/index.js
26 changes: 16 additions & 10 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@
"scripts": {
"start": "node dist",
"clean": "rm -rf dist",
"build": "npm -s run clean && tsc",
"build": "yarn -s clean && tsc && cp -r src/_types dist/_types",
"dev": "ts-node-dev --no-notify --respawn --transpile-only src",
"debug": "ts-node-dev --no-notify --respawn src"
"debug": "ts-node-dev --no-notify --respawn src",
"seed-dummy": "ts-node --transpile-only scripts/seedDummy.ts",
"generate-token": "ts-node --transpile-only scripts/generateToken.ts",
"test-queries": "ts-node --transpile-only scripts/testQueries.ts",
"test-queries:strict": "ts-node --transpile-only scripts/testQueries.ts --strict"
},
"dependencies": {
"@codeday/uploader-node": "^1.0.1",
"apollo-server-express": "^3.6.7",
"@apollo/server": "^4.12.2",
"auth0": "^2.40.0",
"auth0-get-all-users": "^1.1.0",
"class-validator": "^0.13.2",
"class-validator": "^0.14.1",
"dotenv": "^16.0.0",
"emoji-strip": "^1.0.1",
"express": "^4.17.3",
"graphql": "^15.3.0",
"graphql": "^16.11.0",
"graphql-scalars": "^1.25.0",
"graphql-subscriptions": "^2.0.0",
"graphql-upload": "^13.0.0",
"graphql-ws": "^5.6.4",
Expand All @@ -27,24 +32,25 @@
"phone": "^3.1.15",
"reflect-metadata": "^0.1.13",
"stripe": "^15.3.0",
"type-graphql": "^1.1.1",
"type-graphql": "^2.0.0-rc.2",
"typedi": "^0.8.0",
"ws": "^8.5.0"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@codeday/eslint-config": "^2.1.4",
"@codeday/eslint-config-typescript": "^2.1.6",
"@types/auth0": "^2.34.17",
"@types/graphql-upload": "^8.0.11",
"@types/jsonwebtoken": "^8.5.8",
"@types/lru-cache": "^7.6.1",
"@types/node": "^14.0.27",
"@types/node": "^20.17.19",
"@types/node-fetch": "^2.5.7",
"@types/validator": "^13.7.2",
"@types/ws": "^8.5.3",
"eslint": "^7.6.0",
"ts-node": "^8.10.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^1.1.8",
"typescript": "^3.9.7"
"typescript": "^5.7.3"
}
}
}
51 changes: 51 additions & 0 deletions scripts/.seed-dummy.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"event": {
"id": "event-test-2025",
"name": "Local Test Event 2025",
"metadata": {
"location": "Local",
"timezone": "UTC",
"source": "seed-dummy"
}
},
"mentors": [
{
"id": "mentor-alice",
"role": "mentor",
"givenName": "Alice",
"familyName": "Nguyen"
},
{
"id": "mentor-bob",
"role": "mentor",
"givenName": "Bob",
"familyName": "Rivera"
}
],
"students": [
{
"id": "student-ava",
"role": "student",
"givenName": "Ava",
"familyName": "Patel"
},
{
"id": "student-ben",
"role": "student",
"givenName": "Ben",
"familyName": "Kim"
}
],
"project": {
"id": "project-local-1",
"name": "Neighborhood Climate Dashboard",
"mentors": [
"mentor-alice",
"mentor-bob"
],
"students": [
"student-ava",
"student-ben"
]
}
}
46 changes: 46 additions & 0 deletions scripts/generateToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { config as loadEnv } from "dotenv";
import jwt from "jsonwebtoken";
import { AuthRole, JwtToken } from "../src/context/auth/JwtToken";

loadEnv();

const [eventId, roleArg] = process.argv.slice(2);
const role = (roleArg || "").toLowerCase();

if (!eventId || !role) {
console.error("Usage: yarn generate-token -- <event-id> <admin|manager|mentor|student>");
process.exit(1);
}

const authSecret = process.env.AUTH_SECRET;
if (!authSecret) {
console.error("Missing AUTH_SECRET in environment.");
process.exit(1);
}

const roleToTokenType: Record<string, AuthRole> = {
admin: AuthRole.ADMIN,
manager: AuthRole.USER,
mentor: AuthRole.USER,
student: AuthRole.USER,
};

const tokenType = roleToTokenType[role];
if (!tokenType) {
console.error(`Unsupported role: ${role}. Use admin, manager, mentor, or student.`);
process.exit(1);
}

const userId = tokenType === AuthRole.USER ? `${role}-${eventId}` : undefined;
const payload: JwtToken = {
t: tokenType,
...(userId ? { u: userId } : {}),
};

const signed = jwt.sign(payload, authSecret, {
expiresIn: "8h",
issuer: "account-gql-local",
audience: eventId,
});

console.log(signed);
37 changes: 37 additions & 0 deletions scripts/seedDummy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { config as loadEnv } from "dotenv";
import fs from "fs";
import path from "path";

loadEnv();

const fixture = {
event: {
id: "event-test-2025",
name: "Local Test Event 2025",
metadata: {
location: "Local",
timezone: "UTC",
source: "seed-dummy",
},
},
mentors: [
{ id: "mentor-alice", role: "mentor", givenName: "Alice", familyName: "Nguyen" },
{ id: "mentor-bob", role: "mentor", givenName: "Bob", familyName: "Rivera" },
],
students: [
{ id: "student-ava", role: "student", givenName: "Ava", familyName: "Patel" },
{ id: "student-ben", role: "student", givenName: "Ben", familyName: "Kim" },
],
project: {
id: "project-local-1",
name: "Neighborhood Climate Dashboard",
mentors: ["mentor-alice", "mentor-bob"],
students: ["student-ava", "student-ben"],
},
};

const outputPath = path.resolve(process.cwd(), "scripts", ".seed-dummy.json");
fs.writeFileSync(outputPath, JSON.stringify(fixture, null, 2));

console.log(`Wrote local seed fixture: ${outputPath}`);
console.log("This service is Auth0-backed and has no Prisma event/project models, so seed data is generated as local fixtures for testing workflows.");
Loading