Compare commits

...

3 Commits

5 changed files with 43 additions and 15 deletions

View File

@ -13,11 +13,9 @@ ADMIN_PASSWORD=""
ACCOUNT_CREATION_ENABLED="0"
LOG_LEVEL="info"
APP_ENV="production"
DOMAIN=""
FRONTEND_URL=""
DOMAIN="localhost"
FRONTEND_URL="http://localhost:3000"
# Frontend (build-stage)
VITE_ACCOUNT_CREATION_ENABLED="$ACCOUNT_CREATION_ENABLED"
VITE_VIEW_COOKIE_PATH="/"
VITE_COOKIE_SAME_SITE="strict"
VITE_VIEW_COOKIE_DOMAIN="$DOMAIN"

View File

@ -20,18 +20,39 @@ Available:
Waiting to be implemented:
- (Bulk) import/export (Markdown, PDF)
- Webhooks
- Import/export of notes ([#7](https://git.umbrella.haus/ae/qnote/issues/7))
- Bulk import/export with Markdown
- Single note export with PDF
- Webhook compatibility ([#4](https://git.umbrella.haus/ae/qnote/issues/4))
## Usage
The Dockerized app can be run in fullstack (`docker-compose-full.yml`) and backend (`docker-compose-back.yml`) modes. In backend mode only the Golang server (and the Postgres database) are containerized and the frontend can be run separately for quicker development (`npm run dev`). The default fullstack setup defaults to exposing port 3000.
The app can be run in two modes:
The `./scripts/run_dev.sh -h` output can be used as reference of running each of the available modes (or purging data from previous test runs with `-p` or `-q`):
- Combined/fullstack (`docker-compose-full.yml`)
- Frontend published to `0.0.0.0:3000` (`DOMAIN` must be set to the "server's" IP address when accessing e.g. from local network, otherwise the authentication cookies won't be sent correctly by client's browser)
- Dockerized backend & Vite dev server (`docker-compose-back.yml`)
- Frontend published to `localhost:5173`
- Golang server and Postgres database are containerized and the frontend can be run in development mode with `npm run dev`
```
The `./scripts/run_dev.sh -h` output can be used as a reference of running each of the available modes (combined, separate frontend and backend, or purging data from previous test runs with `-p` or `-q`):
```shell
[?] usage: ./scripts/run_dev.sh [-h|-f|-p]
-f run both frontend and backend (default: false)
-p purge any existing database artifacts (default: false)
-q only purge old data without building/spawning any new containers (default: false)
```
## Configuration
| Variable | Description | Default |
| -------------------------- | ---------------------------------------------------------------------- | -------------------------- |
| `PG_*` | Postgres credentials | `qnote` (user and DB name) |
| `*_SECRET` | Backend secrets | - |
| `ADMIN_*` | Initial admin account credentials | - |
| `ACCOUNT_CREATION_ENABLED` | Boolean to toggle registration | `0` |
| `LOG_LEVEL` | Server log level | `info` |
| `APP_ENV` | Boolean to toggle production security features (HTTPS for CSRF & CORS) | `production` |
| `DOMAIN` | Domain of authentication cookies | `localhost` |
| `FRONTEND_URL` | CORS frontend URL | `http://localhost:3000` |

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 333 KiB

View File

@ -14,7 +14,10 @@ import (
"github.com/rs/zerolog/log"
)
const authRateLimit = 5 // req/min
const (
authRateLimit = 5 // req/min
postAuthRateLimit = 100 // req/min
)
type SvcConfig struct {
JWTSecret string
@ -40,7 +43,7 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error {
authRouter := authResource{
Config: config,
RateLimiter: httprate.NewRateLimiter(authRateLimit, time.Minute),
RateLimiter: getRateLimiter(authRateLimit),
Users: q,
Tokens: q,
}
@ -55,7 +58,7 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error {
r.Use(middleware.RequestID)
r.Use(middleware.RealIP)
r.Use(loggerMiddleware(&log.Logger))
r.Use(httprate.LimitByIP(100, time.Minute)) // Base limit for all routes
r.Use(getRateLimiter(postAuthRateLimit).Handler) // Base limit applied globally
r.Use(cors.Handler(cors.Options{
AllowedOrigins: config.allowedOrigins(),
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
@ -86,6 +89,14 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error {
return http.ListenAndServe(":8080", r)
}
// Generate a new rate limiter that responds to surpassing clients with HTTP 429 + JSON error message.
func getRateLimiter(requestLimit int) *httprate.RateLimiter {
return httprate.NewRateLimiter(requestLimit, time.Minute, httprate.WithLimitHandler(func(w http.ResponseWriter, r *http.Request) {
// NOTE: Error message mustn't contain a dot due to one being added client-side anyway
respondError(w, http.StatusTooManyRequests, "You're rate-limited, please slow down")
}))
}
// Start worker that automatically cleans up the `notes` (cascading to `note_versions`) and
// `refresh_tokens` tables from expired (or revoked) entries. The tasks run once during
// initialization and then once an hour until the backend is shutdown.

View File

@ -23,9 +23,7 @@ const CSRF_EXP_MS = 12 * 60 * 60 * 1000 // 12 h.
const REFRESH_BUF = 30 * 1000 // 30 s.
// view cookie configuration (holds UNIX timestamp value of the actual refresh token cookie's expiration date)
const VIEW_COOKIE_PATH = import.meta.env.VITE_VIEW_COOKIE_PATH || "/"
const VIEW_COOKIE_DOMAIN = import.meta.env.VITE_VIEW_COOKIE_DOMAIN || "localhost"
const COOKIE_SAME_SITE = import.meta.env.VITE_COOKIE_SAME_SITE || "strict"
const COOKIE_SECURE = import.meta.env.PROD ? true : false
// some of these could just be local variables as not all of them are being used globally
@ -250,7 +248,7 @@ class ApiClient {
}
// overwrite the view cookie with details that match with the real cookie
document.cookie = `${this.viewCookieName}=;path=${VIEW_COOKIE_PATH};domain=${VIEW_COOKIE_DOMAIN};expires=Thu, 01 Jan 1970 00:00:00 GMT;${COOKIE_SECURE ? "secure;" : ""}sameSite=${COOKIE_SAME_SITE}`
document.cookie = `${this.viewCookieName}=;path=/;domain=${VIEW_COOKIE_DOMAIN};expires=Thu, 01 Jan 1970 00:00:00 GMT;${COOKIE_SECURE ? "secure;" : ""}sameSite=strict`
}
private getCookieValue(cookieName: string): string | null {