223 lines
6.0 KiB
Markdown
223 lines
6.0 KiB
Markdown
# Docker Mailserver User Management Panel
|
|
|
|
A small full-stack admin tool for managing Docker Mailserver accounts with Bun, ElysiaJS, and React.
|
|
|
|
## 1. Proposed stack
|
|
|
|
### Backend
|
|
|
|
- Bun
|
|
- ElysiaJS
|
|
- Bun SQLite
|
|
- Docker CLI through fixed argument arrays
|
|
- Zod for small, explicit validation
|
|
|
|
Why this backend:
|
|
|
|
- Bun keeps the runtime, package manager, and SQLite support simple.
|
|
- ElysiaJS is a good fit for a small internal API without extra framework weight.
|
|
- SQLite is enough for admin sessions and password reset tokens, and keeps deployment straightforward.
|
|
- Docker CLI integration stays close to Docker Mailserver's real operational model instead of hiding it behind unnecessary layers.
|
|
|
|
### Frontend
|
|
|
|
- React
|
|
- Vite
|
|
- Plain CSS
|
|
|
|
Why React:
|
|
|
|
- It is easy for most full-stack teams to read and maintain.
|
|
- This panel only needs basic local state and forms, so React works well without routers or global state libraries.
|
|
- The UI stays minimal while still being easy to grow later.
|
|
|
|
## 2. Folder structure
|
|
|
|
```text
|
|
apps/
|
|
api/
|
|
src/
|
|
auth/
|
|
docker/
|
|
lib/
|
|
mail/
|
|
users/
|
|
web/
|
|
src/
|
|
api/
|
|
components/
|
|
pages/
|
|
styles/
|
|
packages/
|
|
shared/
|
|
data/
|
|
app.db
|
|
infra/
|
|
nginx/
|
|
```
|
|
|
|
## 3. Backend implementation
|
|
|
|
The backend is intentionally flat:
|
|
|
|
- `apps/api/src/index.ts`
|
|
Defines the HTTP routes and keeps request flow obvious.
|
|
- `apps/api/src/auth/service.ts`
|
|
Handles admin login, bootstrap admin seeding, session cookies, and session lookup.
|
|
- `apps/api/src/docker/`
|
|
Wraps Docker CLI calls with fixed argument arrays only. No shell interpolation.
|
|
- `apps/api/src/users/service.ts`
|
|
Contains the mailbox user operations and password reset flow.
|
|
- `apps/api/src/mail/service.ts`
|
|
Sends reset emails through `sendmail` inside the Docker Mailserver container.
|
|
- `apps/api/src/lib/`
|
|
Holds config parsing, SQLite access, validation, HTTP helpers, and small security utilities.
|
|
|
|
### API design
|
|
|
|
- `GET /api/auth/session`
|
|
- `POST /api/auth/login`
|
|
- `POST /api/auth/logout`
|
|
- `GET /api/users`
|
|
- `POST /api/users`
|
|
- `PATCH /api/users/:email/password`
|
|
- `DELETE /api/users/:email`
|
|
- `POST /api/users/:email/password-reset`
|
|
- `POST /api/password-resets/inspect`
|
|
- `POST /api/password-resets/consume`
|
|
|
|
## 4. Frontend implementation
|
|
|
|
The frontend is a small React app with three views:
|
|
|
|
- Login page
|
|
- Dashboard page
|
|
- Public reset-password page
|
|
|
|
The dashboard includes:
|
|
|
|
- mailbox listing
|
|
- simple search
|
|
- create user modal
|
|
- edit password modal
|
|
- delete action
|
|
- send reset action
|
|
|
|
The create flow intentionally generates a temporary password on the server and immediately emails a one-time reset link to the user. That means the admin panel never needs to expose an initial mailbox password in the UI.
|
|
|
|
## 5. Password reset flow
|
|
|
|
The reset flow is built to stay secure without becoming heavy:
|
|
|
|
1. An admin triggers a reset for an existing mailbox.
|
|
2. The backend generates a random opaque token.
|
|
3. Only the SHA-256 hash of that token is stored in SQLite.
|
|
4. The user receives an email with the one-time reset link.
|
|
5. The reset page validates the token before showing the password form.
|
|
6. On submit, the token is claimed for one use only.
|
|
7. The backend updates the mailbox password through Docker Mailserver.
|
|
8. Used or expired tokens are rejected.
|
|
|
|
## 6. Docker CLI integration strategy
|
|
|
|
The app talks to Docker Mailserver through `docker exec` with explicit argument arrays:
|
|
|
|
- `setup email list`
|
|
- `setup email add <email> <password>`
|
|
- `setup email update <email> <password>`
|
|
- `setup email del <email>`
|
|
- `sendmail -t -i`
|
|
|
|
Security choices here:
|
|
|
|
- No `sh -c`
|
|
- No user input inserted into shell strings
|
|
- Input is validated before command execution
|
|
- Docker commands have a timeout
|
|
- Errors are returned as API failures instead of leaking raw implementation detail to the browser
|
|
|
|
## 7. Security notes
|
|
|
|
- Admin authentication uses an Argon2id-hashed bootstrap account from environment variables.
|
|
- Sessions are server-side, opaque, and stored in an `HttpOnly` cookie with `SameSite=Strict`.
|
|
- Password reset tokens are random, one-time, time-limited, and stored only as hashes.
|
|
- Admin-only routes require a valid session cookie.
|
|
- Validation is applied to logins, emails, passwords, and reset payloads.
|
|
- Allowed origins are restricted by `APP_ORIGINS`.
|
|
- Reset emails are sent from the backend only. No secrets are exposed to the frontend.
|
|
- Docker Mailserver access is treated as privileged infrastructure access.
|
|
|
|
## 8. Run locally
|
|
|
|
### Prerequisites
|
|
|
|
- Bun installed
|
|
- Docker available locally
|
|
- A running Docker Mailserver container
|
|
- The API process must be allowed to access Docker
|
|
|
|
### Setup
|
|
|
|
1. Copy `.env.example` to `.env`.
|
|
2. Update at least:
|
|
- `HOSTED_DOMAINS`
|
|
- `MAILSERVER_CONTAINER`
|
|
- `APP_ADMIN_EMAIL`
|
|
- `APP_ADMIN_PASSWORD`
|
|
- `APP_MAIL_FROM`
|
|
3. Install dependencies:
|
|
|
|
```bash
|
|
bun install
|
|
```
|
|
|
|
4. Start the API:
|
|
|
|
```bash
|
|
bun run dev:api
|
|
```
|
|
|
|
5. In another terminal, start the frontend:
|
|
|
|
```bash
|
|
bun run dev:web
|
|
```
|
|
|
|
6. Open `http://localhost:5173`.
|
|
|
|
### Production-style local run
|
|
|
|
If you want the built frontend to be served by the API itself:
|
|
|
|
```bash
|
|
bun run build
|
|
bun run dev:api
|
|
```
|
|
|
|
Then open `http://localhost:3001`.
|
|
|
|
The API serves files from `apps/web/dist` by default, configurable with `WEB_DIST_PATH`.
|
|
|
|
### Docker permission note
|
|
|
|
If you see:
|
|
|
|
```text
|
|
permission denied while trying to connect to the docker API at unix:///var/run/docker.sock
|
|
```
|
|
|
|
the app itself is fine, but the API process cannot talk to Docker.
|
|
|
|
Common fixes:
|
|
|
|
- Run the API with a user that belongs to the `docker` group
|
|
- If the API runs in a container, mount `/var/run/docker.sock:/var/run/docker.sock`
|
|
- Make sure the Docker daemon is actually running
|
|
- Make sure `MAILSERVER_CONTAINER` matches the real Docker Mailserver container name
|
|
|
|
## Notes and assumptions
|
|
|
|
- This panel assumes Docker Mailserver is using the file-based account provisioner and supports the standard `setup email` commands.
|
|
- The admin account for the panel is separate from mailbox users.
|
|
- The app stores only panel-specific metadata in SQLite. Mailbox accounts themselves remain managed by Docker Mailserver.
|