Compare commits
3 Commits
100f534b4c
...
4fb6cf9622
Author | SHA1 | Date | |
---|---|---|---|
4fb6cf9622 | |||
4625720588 | |||
ae3c0e6c0c |
@ -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"
|
31
README.md
31
README.md
@ -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 |
@ -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.
|
||||
|
@ -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 {
|
||||
|
Loading…
x
Reference in New Issue
Block a user