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
13 changes: 7 additions & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,20 @@ CRON_SECRET="" # auto-filled by `npm run setup` — sh
# --- Logging --------------------------------------------------
LOG_LEVEL="info" # debug | info | warn | error

# --- Push Notifications (Web Push / VAPID) --------------------
VAPID_PUBLIC_KEY="" # generate with: npx web-push generate-vapid-keys
VAPID_PRIVATE_KEY=""
VAPID_SUBJECT="mailto:admin@studybot.app"

# --- Email Reminders ------------------------------------------
# --- Email ----------------------------------------------------
EMAIL_PROVIDER="console" # console | smtp
SMTP_HOST=""
SMTP_PORT="587"
SMTP_USER=""
SMTP_PASS=""
SMTP_FROM="Study Bot <noreply@studybot.app>"

# Require users to verify their email address before signing in.
# Default: off (unset/false) — new accounts can sign in immediately.
# Only enable this with a real SMTP provider configured above; with the
# console provider, verification links are only printed to the server logs.
# REQUIRE_EMAIL_VERIFICATION="true"

# --- Google Calendar Integration ------------------------------
GOOGLE_CLIENT_ID=""
GOOGLE_CLIENT_SECRET=""
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ jobs:

- run: npm ci
- run: npx prisma generate
- name: Push schema to test DB
run: npx prisma db push --force-reset --accept-data-loss
- name: Apply migration history to test DB
run: npx prisma migrate deploy
- name: Apply pgvector migration
run: psql "${DATABASE_URL%%\?*}" -f prisma/manual-scripts/001_pgvector.sql
- run: npm run test:integration
Expand Down Expand Up @@ -121,8 +121,8 @@ jobs:

- run: npm ci
- run: npx prisma generate
- name: Push schema to test DB
run: npx prisma db push --force-reset --accept-data-loss
- name: Apply migration history to test DB
run: npx prisma migrate deploy
- name: Apply pgvector migration
run: psql "${DATABASE_URL%%\?*}" -f prisma/manual-scripts/001_pgvector.sql
- run: npx playwright install --with-deps chromium
Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ The full treatment, with effect sizes, primary citations, and the failure modes

## Quick start

Prerequisites: Node 20.19 or newer, plus Docker Desktop (or your own PostgreSQL server).

```
git clone <repo> && cd Study-Bot
npm install
npm run setup
npm run dev
```

Then open http://localhost:3000. The defaults need no API keys or external accounts: the AI provider is a deterministic mock, email logs to the console, and calendar sync uses a local fake. For real AI generated questions and feedback, set `AI_PROVIDER` to `openai` and add your `OPENAI_API_KEY` in `.env`.
Then open http://localhost:3000. The defaults need no API keys or external accounts: the AI provider is a deterministic mock, email logs to the console, and calendar sync uses a local fake.

To be blunt about the default: mock mode exists for tests. With `AI_PROVIDER` set to `mock`, sessions get template questions and canned feedback, which exercises the machinery but teaches you nothing. To actually study, set `AI_PROVIDER` to `openai` and add an `OPENAI_API_KEY` in `.env`.

What `npm run setup` does: copies `.env.example` to `.env`, generates `NEXTAUTH_SECRET`, `TOKEN_ENC_KEY`, and `CRON_SECRET`, starts PostgreSQL through Docker, and applies all migrations. It is idempotent and never overwrites a value you set yourself.

Expand Down Expand Up @@ -139,6 +143,13 @@ Each technique below is implemented in the product. Citations are shortened here

One warning: `ALLOW_TEST_AUTH` makes the app trust a header as the authenticated identity. It exists only for automated tests and must never be set in production.

## Deployment and ops notes

* **Auth gates.** `REQUIRE_EMAIL_VERIFICATION` is off by default, so new accounts can sign in immediately; enable it only once real SMTP is configured, because the console email provider prints verification links to server logs only. `ALLOW_TEST_AUTH` must never be set in production. `TOKEN_ENC_KEY` is required in production (setup generates one for local use).
* **One long lived Node process.** The app is not serverless safe. Feedback is generated eagerly when an attempt lands, and the AI rate limiter and circuit breaker hold their state in process, so run it as a single persistent Node server via `npm start` rather than as per request functions.
* **The port 3000 trap.** If something else already holds port 3000, Next silently starts on 3001 while `NEXTAUTH_URL` still points at 3000, and sign in breaks with baffling redirects. Free port 3000, or update `NEXTAUTH_URL` (plus `BASE_URL` and `NEXT_PUBLIC_APP_URL`) to the port actually in use.
* **Admin routes.** The `/admin` pages and admin APIs are gated by `ADMIN_USER_IDS`, a comma separated list of user UUIDs. Until you set it, nobody is an admin.

## Development

* `npm run dev`, `npm run build`, and `npm start` cover the dev server and production.
Expand Down
15 changes: 3 additions & 12 deletions e2e/knowledge-layer.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@ const BASE_URL = process.env.BASE_URL || "http://localhost:3000";
const USER_ID = "e2e_test_user";
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });

// Override extraHTTPHeaders to remove X-User-Id — we set it via localStorage
// and the client JS adds it to fetch calls. Having Playwright ALSO inject it
// causes duplicate headers which break the ownership check.
test.use({ extraHTTPHeaders: {} });
// Playwright context headers ride on every browser request, so app fetches
// authenticate as the test user (clients no longer send identity headers).
test.use({ extraHTTPHeaders: { "X-User-Id": USER_ID } });

test.describe.serial("Knowledge Layer — Leak Prevention", () => {
const COURSE = "E2E_CS_KL";
Expand Down Expand Up @@ -78,10 +77,6 @@ test.describe.serial("Knowledge Layer — Leak Prevention", () => {
test("review panel NOT visible before submitting answer", async ({ page }) => {
const sessionUrl = `${BASE_URL}/s/${sessionId}`;
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session|resume session/i }).click();

Expand All @@ -98,10 +93,6 @@ test.describe.serial("Knowledge Layer — Leak Prevention", () => {
test("review panel appears after INCORRECT scoring with deferred feedback", async ({ page }) => {
const sessionUrl = `${BASE_URL}/s/${sessionId}`;
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session|resume session/i }).click();

Expand Down
28 changes: 3 additions & 25 deletions e2e/session-runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,9 @@ const SESSION_PAYLOAD = {
let sessionId: string;
let sessionUrl: string;

// Override extraHTTPHeaders to remove X-User-Id — we set it via localStorage
// and the client JS adds it to fetch calls. Having Playwright ALSO inject it
// causes duplicate headers which break the ownership check.
test.use({ extraHTTPHeaders: {} });
// Playwright context headers ride on every browser request, so app fetches
// authenticate as the test user (clients no longer send identity headers).
test.use({ extraHTTPHeaders: { "X-User-Id": USER_ID } });

test.describe.serial("E2E: Full Retrieval Session Runner", () => {
test.beforeAll(async () => {
Expand Down Expand Up @@ -80,11 +79,6 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {
});

test("start session and see first prompt", async ({ page }) => {
// Need to set the user ID in localStorage before starting
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session/i }).click();
Expand All @@ -96,10 +90,6 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {

test("submit a CORRECT answer", async ({ page }) => {
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session|resume session/i }).click();

Expand All @@ -125,10 +115,6 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {

test("submit an INCORRECT answer with error log", async ({ page }) => {
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session|resume session/i }).click();
await expect(page.locator("textarea")).toBeVisible({ timeout: 5000 });
Expand Down Expand Up @@ -160,10 +146,6 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {

test("refresh page mid-run preserves progress", async ({ page }) => {
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

// Should show Resume (not Start) since we have an active run
await expect(page.getByRole("button", { name: /resume session/i })).toBeVisible({
Expand All @@ -178,10 +160,6 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {

test("complete final prompt and see end screen", async ({ page }) => {
await page.goto(sessionUrl);
await page.evaluate((uid) => {
localStorage.setItem("study_bot_user_id", uid);
}, USER_ID);
await page.goto(sessionUrl);

await page.getByRole("button", { name: /start session|resume session/i }).click();
// The review panel renders reflection textareas too, so target the
Expand Down
Loading
Loading