202 lines
9.1 KiB
Markdown
202 lines
9.1 KiB
Markdown
# Minabot
|
|
|
|
Bun + Hono habit-tracking REST API with Drizzle ORM and local SQLite. The existing
|
|
React Router client remains a minimal authentication shell; habit frontend UI is deferred.
|
|
|
|
See [the REST API reference](docs/API.md) for habits, task recurrence, dated progress,
|
|
combined calendars, historical corrections, daily resets, and optional count carryover.
|
|
|
|
```sh
|
|
bun install
|
|
bun dev
|
|
```
|
|
|
|
Open http://127.0.0.1:3000. Set `PORT` to use a different port.
|
|
|
|
- Client pages: `/`, `/about`, `/settings`.
|
|
- Hono endpoint: `GET /api/health`.
|
|
- Unknown API routes return JSON with a 404 status.
|
|
|
|
Pages contain a heading, native navigation buttons, and account controls. Tailwind utilities
|
|
and shadcn configuration are available; Tailwind Preflight is omitted to preserve
|
|
native browser styling.
|
|
|
|
```sh
|
|
bun run typecheck
|
|
bun run build
|
|
bun start
|
|
```
|
|
|
|
The build includes the backend and frontend in `dist/`. `bun start` serves that
|
|
production build. Client routes support direct loading and browser history.
|
|
|
|
## Database
|
|
|
|
The backend uses Drizzle ORM with Bun's built-in SQLite driver. The database is
|
|
created at `data/minabot.sqlite`, with WAL mode and foreign keys enabled. Database
|
|
files are ignored by Git. Development and production use the same file.
|
|
|
|
Optionally copy `.env.example` to `.env` and set `DATABASE_PATH` to another local
|
|
file. Environment-specific database paths can be set in `.env.development` and
|
|
`.env.production`. Relative paths are resolved from the project root by the package scripts.
|
|
|
|
Define tables in `src/db/schema.ts`, then generate and apply migrations:
|
|
|
|
```sh
|
|
bun run db:generate --name describe_change
|
|
bun run db:migrate
|
|
```
|
|
|
|
Commit the generated `drizzle/` files alongside schema changes. `bun dev` and
|
|
`bun start` apply pending migrations before starting the server. The initial
|
|
migration creates the users and sessions tables.
|
|
|
|
Import `db` from `src/db/index.ts` in backend code to query the database.
|
|
`GET /api/health` checks the database connection before returning success.
|
|
|
|
## Discord sign-in
|
|
|
|
1. Copy `.env.example` to `.env` for shared defaults, and copy
|
|
`.env.development.example` / `.env.production.example` to `.env.development` /
|
|
`.env.production` if those files do not exist. These local files are Git-ignored.
|
|
2. Create an application in the [Discord Developer Portal](https://discord.com/developers/applications).
|
|
Under OAuth2, copy the client ID and client secret into `DISCORD_CLIENT_ID`
|
|
and `DISCORD_CLIENT_SECRET` in `.env`.
|
|
3. Set `AUTH_COOKIE_SECRET` to a random secret of at least 32 characters, generated
|
|
with `openssl rand -hex 32`. Keep it server-side along with the client secret.
|
|
4. Set `APP_ORIGIN` in `.env.development` to the development browser origin,
|
|
initially `http://127.0.0.1:3000`, and set the same variable in `.env.production`
|
|
to the production origin, such as `https://habits.example.com`. Register **both**
|
|
origins with `/api/auth/discord/callback` appended as redirect URIs in Discord.
|
|
Use the matching hostname throughout each sign-in; `localhost` and `127.0.0.1`
|
|
have different cookie storage.
|
|
5. Run `bun dev`, click **Sign in with Discord**, and authorize the `identify` scope.
|
|
No bot token or email scope is needed. After returning, **View my profile** opens
|
|
`/api/me`. **Sign out** revokes the current browser's session.
|
|
|
|
Without the OAuth settings, public pages and `/api/health` remain usable, `/api/me`
|
|
returns 401, and sign-in displays a configuration message. Restart after editing
|
|
any env file. Use HTTPS for non-local deployments; HTTP is only allowed on loopback
|
|
origins. When using a reverse proxy, preserve the public request host and serve
|
|
both React and `/api` from the selected origin.
|
|
|
|
The package scripts explicitly select the environment before migrations or server
|
|
startup:
|
|
|
|
| Command | Environment file | Mode |
|
|
| --- | --- | --- |
|
|
| `bun dev` | `.env.development` | development |
|
|
| `bun start` | `.env.production` | production |
|
|
| `bun run db:generate` / `bun run db:migrate` | `.env.development` | development |
|
|
| `bun run db:migrate:production` | `.env.production` | production |
|
|
|
|
Each command loads optional `.env` shared defaults, then the selected file.
|
|
Variables exported by the shell or deployment platform take precedence over files.
|
|
The selected `APP_ORIGIN` controls OAuth callbacks, cookie security, and logout
|
|
Origin validation. Production requires an explicit `APP_ORIGIN`; development can
|
|
fall back to `http://127.0.0.1:$PORT` (port 3000 if unset). To test `bun start`
|
|
locally, set `APP_ORIGIN=http://127.0.0.1:3000` in `.env.production`.
|
|
|
|
Automatic env-file loading is disabled in `bunfig.toml` so the outer `bun start`
|
|
process cannot preload development settings into production. Package scripts load
|
|
the files explicitly using Bun's `--env-file` option; `.env.local` files are not
|
|
loaded by these commands. Build, test, and typecheck do not load application env
|
|
files. For custom Bun commands, pass the desired `--env-file` flags explicitly.
|
|
See [Bun environment variables](https://bun.sh/docs/runtime/environment-variables).
|
|
|
|
Use `APP_ORIGIN` in each file; `APP_ORIGIN_DEV` and `APP_ORIGIN_PROD` are no longer
|
|
used. Credentials can stay in `.env` or be overridden separately in each file.
|
|
For deployments sharing a machine, set distinct `DATABASE_PATH` values to keep
|
|
production habit data separate from development data.
|
|
|
|
### Schema and session behavior
|
|
|
|
- `users`: app UUID, unique Discord ID stored as text, username, nullable display
|
|
name and avatar hash, IANA timezone, and creation/update timestamps.
|
|
- `sessions`: SHA-256 token hash, user foreign key with cascading deletion,
|
|
creation timestamp, and expiry timestamp. User and expiry indexes support
|
|
session management and cleanup.
|
|
- Timestamps are Unix milliseconds. Each session expires 30 days after sign-in;
|
|
activity does not extend it. A new sign-in replaces the current browser's session.
|
|
Other browsers retain independent sessions. Expired sessions are removed during
|
|
sign-in or when presented to an authenticated route.
|
|
- Cookies are HttpOnly, SameSite=Lax, and host-only. HTTPS enables Secure and
|
|
`__Host-` cookie names. OAuth state uses a signed cookie with a ten-minute
|
|
lifetime and is cleared on callback. Token exchange and profile retrieval happen
|
|
server-side; Discord access/refresh tokens are never persisted or sent to React.
|
|
- React detects the browser timezone during initial registration. The server
|
|
validates it using `Intl.DateTimeFormat`, falling back to UTC. Later sign-ins
|
|
update the Discord profile without changing the saved timezone. A timezone
|
|
settings UI is deferred; `PATCH /api/me` updates the timezone. Habit timestamps
|
|
remain UTC and daily boundaries follow the saved timezone with preserved historical
|
|
deadlines. See the API reference for travel and DST behavior.
|
|
|
|
### API
|
|
|
|
| Method | Route | Behavior |
|
|
| --- | --- | --- |
|
|
| GET | `/api/health` | Public database health check |
|
|
| GET | `/api/auth/discord?timezone=Europe%2FBelgrade` | Start sign-in |
|
|
| GET | `/api/auth/discord/callback` | Validate OAuth response and create a session |
|
|
| POST | `/api/auth/logout` | Revoke current session; requires matching Origin |
|
|
| GET | `/api/me` | Public user fields for the current session, or 401 |
|
|
|
|
`/api/me` returns the object directly:
|
|
|
|
```json
|
|
{
|
|
"id": "app-generated-uuid",
|
|
"discordId": "123456789012345678",
|
|
"username": "mina",
|
|
"displayName": "Mina",
|
|
"avatarUrl": null,
|
|
"timezone": "Europe/Belgrade"
|
|
}
|
|
```
|
|
|
|
Only the fields declared in `src/shared/user.ts` are returned. Missing/invalid or
|
|
expired sessions receive `401 {"error":"Unauthorized"}`. Authentication endpoints
|
|
and `/api/me` use `Cache-Control: no-store`.
|
|
|
|
In `src/api.ts`, protect another route with the same middleware:
|
|
|
|
```ts
|
|
app.get("/api/example", auth.requireAuth, c => c.json({ userId: c.get("user").id }));
|
|
app.post("/api/example", auth.requireSameOrigin, auth.requireAuth, c => {
|
|
// Validate input and enforce ownership using c.get("user").id.
|
|
return c.json({ ok: true });
|
|
});
|
|
```
|
|
|
|
Use `requireSameOrigin` for all cookie-authenticated mutations. Same-origin browser
|
|
fetches send the cookie automatically; tokens should not be placed in localStorage.
|
|
|
|
```sh
|
|
bun test
|
|
bun run typecheck
|
|
bun run build
|
|
```
|
|
|
|
Tests use an isolated in-memory SQLite database with the real migrations and a
|
|
stubbed Discord HTTP client. They cover registration, profile refresh, timezone
|
|
preservation, cookie security, invalid/expired state, provider failures, session
|
|
expiry/rotation, logout CSRF checks, and the exact `/api/me` response. A real Discord
|
|
consent flow additionally requires your application credentials and browser login.
|
|
|
|
|
|
## Habit API verification
|
|
|
|
```sh
|
|
bun run test:coverage
|
|
bun run typecheck
|
|
bun run test:smoke
|
|
```
|
|
|
|
The smoke command builds and starts the production server against a disposable
|
|
SQLite database, exercises authenticated HTTP requests, then restarts it to verify
|
|
persistence. It does not use a live Discord provider or modify application data.
|
|
Habit tests cover every route, all PRD acceptance behaviors, ownership and Origin
|
|
checks, historical corrections, expiry, timezone boundaries, daily resets, optional
|
|
count carryover, Nivo data adaptation, and migration upgrades. Semantic commits
|
|
separate the database foundation, REST implementation, carryover, and verification.
|