Compare commits

..

No commits in common. "4fb6cf962268d183c1be8f63fa86dce51985efbb" and "100f534b4cef4f56b032283833a3faea8abd2fb2" have entirely different histories.

5 changed files with 15 additions and 43 deletions

View File

@ -13,9 +13,11 @@ ADMIN_PASSWORD=""
ACCOUNT_CREATION_ENABLED="0"
LOG_LEVEL="info"
APP_ENV="production"
DOMAIN="localhost"
FRONTEND_URL="http://localhost:3000"
DOMAIN=""
FRONTEND_URL=""
# 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,39 +20,18 @@ Available:
Waiting to be implemented:
- 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))
- (Bulk) import/export (Markdown, PDF)
- Webhooks
## Usage
The app can be run in two modes:
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.
- 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 reference of running each of the available modes (or purging data from previous test runs with `-p` or `-q`):
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: 333 KiB

After

Width:  |  Height:  |  Size: 254 KiB

View File

@ -14,10 +14,7 @@ import (
"github.com/rs/zerolog/log"
)
const (
authRateLimit = 5 // req/min
postAuthRateLimit = 100 // req/min
)
const authRateLimit = 5 // req/min
type SvcConfig struct {
JWTSecret string
@ -43,7 +40,7 @@ func Run(conn *pgx.Conn, q *data.Queries, config SvcConfig) error {
authRouter := authResource{
Config: config,
RateLimiter: getRateLimiter(authRateLimit),
RateLimiter: httprate.NewRateLimiter(authRateLimit, time.Minute),
Users: q,
Tokens: q,
}
@ -58,7 +55,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(getRateLimiter(postAuthRateLimit).Handler) // Base limit applied globally
r.Use(httprate.LimitByIP(100, time.Minute)) // Base limit for all routes
r.Use(cors.Handler(cors.Options{
AllowedOrigins: config.allowedOrigins(),
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
@ -89,14 +86,6 @@ 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,7 +23,9 @@ 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
@ -248,7 +250,7 @@ class ApiClient {
}
// overwrite the view cookie with details that match with the real cookie
document.cookie = `${this.viewCookieName}=;path=/;domain=${VIEW_COOKIE_DOMAIN};expires=Thu, 01 Jan 1970 00:00:00 GMT;${COOKIE_SECURE ? "secure;" : ""}sameSite=strict`
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}`
}
private getCookieValue(cookieName: string): string | null {