Skip to content
Draft
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
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ npm ci
### Dev servers

```bash
npm run dev # builds shared once, then runs api/app/web
npm run dev --workspace @doenet-tools/api # Express API on port 3000
npm run dev --workspace @doenet-tools/app # Vite dev server on port 8000 (proxies /api to port 3000)
npm run watch --workspace @doenet-tools/shared # shared TypeScript watcher when needed
```

### Build
Expand Down
30 changes: 9 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,72 +19,60 @@ We would love to hear from you! Join our [Discord](https://discord.gg/PUduwtKJ5h

### Steps

**1. Clone the repository**

```bash
git clone https://github.com/Doenet/DoenetTools.git
cd DoenetTools
```

**2. Install dependencies**
**1. Install dependencies**

```bash
npm install
```

**3. Create the `.env` file**
**2. Create the un-tracked `.env` file**

```bash
npm run setup
```

This copies `apps/api/.env.example` to `apps/api/.env`. The defaults work for local development, but edit as needed (e.g. change `DATABASE_HOST`, `DATABASE_PORT`, or `DATABASE_PASSWORD` if your MySQL setup differs — and update `DATABASE_URL` to match).
This file (`apps/api/.env`) contains secrets like the database password and database url.

**4. Start the database**
**3. Start the database**

```bash
docker compose --env-file apps/api/.env up -d
```

Wait until the MySQL container shows `(healthy)` in `docker container ls` before continuing.

**5. Setup the database tables**
**4. Setup the database tables**

```bash
npm run db:setup
```

This creates the required database tables and seeds them with minimal data.

**6. Start the dev servers**
**5. Start the dev servers**

All dev processes can be started together with a single command:
The main dev processes can be started together with a single command:

```bash
npm run dev
```

This starts:

- Shared package watcher
- Express API → http://localhost:3000
- React SPA → http://localhost:8000 (proxies `/api/*` to the API)
- Astro site → http://localhost:4321

This command first builds `shared` once, then starts its watch process alongside
the app, API, and web dev servers.

Alternatively, run each in a separate terminal if needed:

```bash
npm run dev --workspace @doenet-tools/shared # Shared package watcher
npm run build --workspace @doenet-tools/shared # Do this first
npm run dev --workspace @doenet-tools/api # Express API
npm run dev --workspace @doenet-tools/app # React SPA
npm run dev --workspace @doenet-tools/web # Astro site
npm run watch --workspace @doenet-tools/shared # if making edits to this package
```

---

## Repository Structure

This repository is an npm workspace monorepo. Packages are organized into two directories:
Expand Down
22 changes: 19 additions & 3 deletions apps/api/.env.example
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
# ________________________________________________________
#
# This is an example .env setup for local development.
# You can copy this to `.env` and edit values as needed.
# _________________________________________________________
#
# To edit values for the production site, see
# `/infra/cloudformation/prod-doenet-taskdef-includes.yml`
#
# To edit values for deployed dev site (dev3), see
# `/infra/cloudformation/dev3-doenet-taskdef-includes.yml`



#General
PORT="3000"
# Base URL of the frontend app
APP_URL="http://localhost:8000"
APP_URL="http://localhost:8000" # Base URL of the frontend app
LOG_LEVEL="info"

#Flags
# Dev flags (only enable for local dev, not production)
ENABLE_TEST_AUTH_BYPASS="true"
ENABLE_TEST_ROUTES="true"
MOCK_SIGNIN_EMAIL="true"
PRETTY_LOGS="true"

#Database
# DATABASE_URL must match the individual vars below. Update both if you change any connection details.
Expand All @@ -33,4 +49,4 @@ MAGIC_LINK_SECRET="mysecret"
DISCOURSE_URL="https://community.example.com"
DISCOURSE_SSO_SECRET="mysecret"
DISCOURSE_API_KEY="mysecret"
DISCOURSE_API_USERNAME="system"
DISCOURSE_API_USERNAME="system"
2 changes: 2 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"passport-local": "^1.0.0",
"passport-magic-link": "^2.1.1",
"passport-strategy": "^1.0.0",
"pino-http": "^11.0.0",
"prisma": "6.4.1",
"short-uuid": "^5.2.0",
"zod": "^4.0.14"
Expand All @@ -59,6 +60,7 @@
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-local": "^1.0.38",
"@types/passport-strategy": "^0.2.38",
"pino-pretty": "^13.1.3",
"vitest": "^3.2.4"
},
"optionalDependencies": {
Expand Down
6 changes: 5 additions & 1 deletion apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SESClient, SendEmailCommand } from "@aws-sdk/client-ses";
import { nanoid } from "nanoid";
import * as fs from "fs/promises";
import { fromUUID, toUUID } from "./utils/uuid";
import { formatApiReadyMessage } from "./utils/cli";
import { UserInfo, UserInfoWithEmail } from "./types";
import {
findOrCreateUser,
Expand Down Expand Up @@ -49,6 +50,7 @@ import { generateHandle } from "./utils/names";
import { codeRouter } from "./routes/code";
import { metricsRouter } from "./routes/metricsRoutes";
import { contentRouter } from "./routes/content.route";
import { initRequestLogger } from "./middleware/requestLogger";

// Type assertion to work around passport type declaration issues
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -394,6 +396,7 @@ app.use(

app.use(passport.initialize());
app.use(passport.session());
app.use("/api", initRequestLogger());

const port = process.env.PORT || 3000;

Expand Down Expand Up @@ -473,5 +476,6 @@ app.post(
// );

app.listen(port, () => {
console.log(`[server]: Server is running at http://localhost:${port}`);
const localUrl = `http://localhost:${port}`;
console.log(formatApiReadyMessage(localUrl));
});
88 changes: 88 additions & 0 deletions apps/api/src/logging/loggerConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
export type LogLevel =
| "trace"
| "debug"
| "info"
| "warn"
| "error"
| "fatal"
| "silent";

export type LoggerConfigOptions = {
prettyIgnore?: string[];
stream?: NodeJS.WritableStream;
};

const sensitiveHeaderPaths = [
"req.headers.cookie",
"req.headers.authorization",
'req.headers["proxy-authorization"]',
'req.headers["x-api-key"]',
'req.headers["x-auth-token"]',
'req.headers["x-csrf-token"]',
'req.headers["csrf-token"]',
'res.headers["set-cookie"]',
];

/**
* Shared pino configuration for logs
*/
export function getApiLoggerOptions(options: LoggerConfigOptions = {}) {
const logLevel = getLogLevel();

return {
level: logLevel,
redact: {
paths: sensitiveHeaderPaths,
remove: true,
},
stream: options.stream,
...(shouldUsePrettyLogs(options)
? getPrettyTransportOptions(logLevel, options.prettyIgnore)
: {}),
};
}

function getLogLevel(): LogLevel {
const logLevel = process.env.LOG_LEVEL?.trim().toLowerCase();
return isLogLevel(logLevel) ? logLevel : "info";
}

function isLogLevel(logLevel: string | undefined): logLevel is LogLevel {
return (
logLevel === "trace" ||
logLevel === "debug" ||
logLevel === "info" ||
logLevel === "warn" ||
logLevel === "error" ||
logLevel === "fatal" ||
logLevel === "silent"
);
}

function shouldUsePrettyLogs(options: LoggerConfigOptions) {
if (options.stream) {
return false;
}

return process.env.PRETTY_LOGS?.trim().toLowerCase() === "true";
}

function getPrettyTransportOptions(
logLevel: LogLevel,
prettyIgnore: string[] | undefined,
) {
return {
transport: {
target: "pino-pretty",
options: {
colorize: true,
hideObject: true,
messageFormat: "{msg}",
timestampKey: "__pretty_timestamp_disabled__",
translateTime: "SYS:standard",
...(logLevel === "silent" ? {} : { minimumLevel: logLevel }),
...(prettyIgnore?.length ? { ignore: prettyIgnore.join(",") } : {}),
},
},
};
}
Loading
Loading