22 Commits
Author SHA1 Message Date
Tomáš Dvořák 61f0974c61 Merge pull request #45 from Dvorinka/release/v0.8.3
chore(release): v0.8.3
2026-09-28 20:40:17 +02:00
Tomas DvorakandDevin 769d04471f chore(release): v0.8.3
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 20:36:35 +02:00
Tomáš Dvořák da294aaa2b Merge pull request #44 from Dvorinka/fix/install-prompt-tty
fix(install): prompt on /dev/tty under curl|bash
2026-09-28 20:22:14 +02:00
Tomas DvorakandDevin 1226d44872 fix(install): prompt on /dev/tty so it works under curl|bash
stdin is the script pipe when run as `curl ... | bash`, so the -t 0
check silently skipped the domain question. Read from the controlling
terminal instead; no tty (CI/cron) still defaults to localhost.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 20:19:36 +02:00
Tomáš Dvořák ac4ca9a3ae Merge pull request #43 from Dvorinka/feat/install-public-url
feat(install): ask for the public URL on fresh installs
2026-09-28 19:34:27 +02:00
Tomas DvorakandDevin 597db3287e feat(install): ask for the public URL, wire it into .env
install.sh now asks one question on fresh installs — the URL the app is
opened on. Accepts https://primora.example.com (TLS at the user's proxy),
primora.example.com, or 192.168.1.50:8085; the port becomes NGINX_PORT.
The answer rewrites every public URL var and COOKIE_DOMAIN for real DNS
names. PUBLIC_URL env keeps it non-interactive; legacy DOMAIN/NGINX_PORT
still work; reruns over an existing .env skip the question and display
the URL from the file.

Also trust both URL schemes on the serving host in trustedOrigins —
external TLS termination makes nginx report http while browsers arrive
over https.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 19:31:54 +02:00
Tomáš Dvořák bbc57f9c0d Merge pull request #42 from Dvorinka/fix/sbom-permissions
CI / Backend Tests (push) Canceled after 0s
CI / Frontend Tests (push) Canceled after 0s
CI / Build All Services (push) Canceled after 0s
CI / Docker Build (auth) (push) Canceled after 0s
CI / Docker Build (backend) (push) Canceled after 0s
CI / Docker Build (frontend) (push) Canceled after 0s
CI / Migration Up/Down (push) Canceled after 0s
CI / Compose E2E Smoke (push) Canceled after 0s
CI / Lint (push) Canceled after 0s
CI / Publish edge images (auth) (push) Canceled after 0s
CI / Publish edge images (backend) (push) Canceled after 0s
CI / Publish edge images (frontend) (push) Canceled after 0s
fix(release): contents:write for SBOM asset upload
2026-09-28 16:44:05 +02:00
Tomas DvorakandDevin 9ab9b550d1 fix(release): contents:write for SBOM asset upload
The sbom job's `permissions: packages: read` replaces the default token
set, so `gh release upload` got 403 Resource not accessible. Grant
contents:write alongside packages:read.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 16:41:17 +02:00
Tomáš Dvořák 49ca9067a9 Merge pull request #41 from Dvorinka/release/v0.8.2
chore(release): v0.8.2
2026-09-28 16:28:11 +02:00
Tomas DvorakandDevin 805a58e32f chore(release): v0.8.2
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 16:26:11 +02:00
Tomáš Dvořák d09b0a1c2d Merge pull request #40 from Dvorinka/fix/signup-bounce
fix(auth): sign-up bounce — surface errors, dynamic trusted origins, unbreakable mail probe
2026-09-28 16:25:24 +02:00
Tomas DvorakandDevin 92b1810c07 fix(auth): surface sign-up failures, trust serving origin, unbreakable mail probe
Self-hosted sign-up bounced back to the bootstrap screen for three
compounding reasons:

- better-auth's client resolves { data, error } on HTTP failure; the UI
  ignored res.error and showed "Account created — signing you in", hiding
  the real 403/429/500.
- trustedOrigins was a static localhost-only list. Browsing the instance
  via LAN IP or custom port failed every auth POST once cookies existed.
  Origins are now derived from the request's public host (forwarded by
  nginx), so any origin serving the app works.
- hasMailTransport threw when core.settings was unreadable (fresh volume,
  backend still migrating), so sign-up 500'd and the startup backfill
  could crash-loop the service. Both now degrade to "no transport".

Verified live: LAN-style origin sign-up succeeds, missing core.settings
logs a warn instead of exiting, and the frontend shows real errors.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 16:23:01 +02:00
Tomáš Dvořák 3ad37c190b Merge pull request #39 from Dvorinka/fix/sbom-output-dir
fix: create dist-release dir before SBOM output writes
2026-09-28 16:02:16 +02:00
Tomas DvorakandDevin cb56cedb1f fix: create dist-release dir before SBOM output writes
anchore/sbom-action does not create the output directory — the v0.8.1
run scanned images fine (lowercase owner works) but ENOENT'd writing
dist-release/sbom-*.cdx.json.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 16:00:00 +02:00
Tomáš Dvořák 20b4ea6a07 Merge pull request #38 from Dvorinka/release/v0.8.1
release: v0.8.1
2026-09-28 15:48:12 +02:00
Tomas DvorakandDevin 0511e5754b release: v0.8.1 — SMTP-less auth fix
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 15:45:54 +02:00
Tomáš Dvořák 3800e736d0 Merge pull request #37 from Dvorinka/fix/smtpless-auth-verification
fix: skip email verification entirely when no mail transport exists
2026-09-28 15:45:16 +02:00
Tomas DvorakandDevin 7548ea75d4 fix: skip email verification entirely when no mail transport exists
SMTP-less self-hosted instances still marked every account
emailVerified=false and attempted a verification mail that could
never arrive. Now: no transport → accounts are created verified,
the send is skipped, and existing unverified rows are backfilled
on startup. .env.example stops defaulting SMTP_HOST to the
dev-only mailpit service.

Verified live: fresh signup returns emailVerified=true, legacy
unverified users flip on restart, sign-in unaffected.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 15:43:26 +02:00
Tomáš Dvořák 14d0762245 Merge pull request #36 from Dvorinka/chore/gitignore-brag-output
chore: gitignore local brag-output directory
2026-09-28 15:29:31 +02:00
Tomas DvorakandDevin a990989d34 chore: gitignore local brag-output directory
Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 15:27:21 +02:00
Tomáš Dvořák 3b9ea333ad Merge pull request #35 from Dvorinka/fix/sbom-ghcr-case
fix: lowercase GHCR owner in SBOM image refs
2026-09-28 13:55:59 +02:00
Tomas DvorakandDevin 86d612ec93 fix: lowercase GHCR owner in SBOM image refs
ghcr.io rejects mixed-case repo paths — syft failed to parse
ghcr.io/Dvorinka/primora-* so the v0.8.0 SBOM job errored.
Apply the same lowercase-owner normalization the image jobs use.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-28 13:52:51 +02:00
13 changed files with 198 additions and 39 deletions

No files matched your search

+5 -1
View File
@@ -77,9 +77,13 @@ BACKEND_STORAGE_DRIVER=local
# Mail defaults — each is overridable in-app (Settings → Instance). In-app
# values win over these env vars; deleting the in-app value reverts here.
# With no transport configured (no Resend key and no SMTP_HOST) accounts are
# verified automatically and no verification mail is attempted.
# The dev overlay (docker compose -f docker-compose.yml -f docker-compose.dev.yml)
# runs Mailpit — then SMTP_HOST=mailpit catches all outgoing mail at :8025.
RESEND_API_KEY=
MAIL_FROM=Primora <[email protected]>
SMTP_HOST=mailpit
SMTP_HOST=
SMTP_PORT=1025
SMTP_USER=
SMTP_PASSWORD=
+10 -3
View File
@@ -327,28 +327,35 @@ jobs:
runs-on: ubuntu-latest
permissions:
packages: read
# contents: write — the job uploads SBOM assets onto the release.
contents: write
env:
VERSION: ${{ needs.verify.outputs.version }}
steps:
- uses: actions/checkout@v4
# ghcr.io requires lowercase repo paths; the owner login may be mixed-case.
- name: Compute lowercase GHCR owner
run: echo "GHCR_OWNER=${GITHUB_REPOSITORY_OWNER,,}" >> "$GITHUB_ENV"
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# anchore/sbom-action does not create the output directory.
- run: mkdir -p dist-release
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-backend:${{ needs.verify.outputs.version }}
image: ghcr.io/${{ env.GHCR_OWNER }}/primora-backend:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-backend.cdx.json
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-auth:${{ needs.verify.outputs.version }}
image: ghcr.io/${{ env.GHCR_OWNER }}/primora-auth:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-auth.cdx.json
- uses: anchore/sbom-action@v0
with:
image: ghcr.io/${{ github.repository_owner }}/primora-frontend:${{ needs.verify.outputs.version }}
image: ghcr.io/${{ env.GHCR_OWNER }}/primora-frontend:${{ needs.verify.outputs.version }}
format: cyclonedx-json
output-file: dist-release/sbom-frontend.cdx.json
- uses: anchore/sbom-action@v0
+2
View File
@@ -39,3 +39,5 @@ infra/mailpit
# Generated code-graph artifacts
graphify-out/
.playwright-mcp/
# Local brag/launch-video output
brag-output/
+24
View File
@@ -4,6 +4,30 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
## [Unreleased]
## [0.8.3] - 2026-09-28
### Added
- **Install script asks for the public URL** — `install.sh` now prompts for where you'll open the app and accepts a full URL: `https://primora.example.com` (TLS at your proxy, hints printed at the end), `primora.example.com`, or `192.168.1.50:8085` (the port becomes `NGINX_PORT`). The answer rewrites every public URL var in `.env` and `COOKIE_DOMAIN` for real DNS names. The prompt reads the controlling terminal so it appears under `curl | bash`; no tty (CI, cron) falls back to `localhost`. Non-interactive via `PUBLIC_URL=`; legacy `DOMAIN`/`NGINX_PORT` envs still work; reruns over an existing `.env` skip the question.
### Fixed
- **Auth origins behind external TLS terminators** — trusted origins now accept both URL schemes on the serving host. Verified live over a Cloudflare quick tunnel: a real browser session cookie + `https://*.trycloudflare.com` origin was 403'd on v0.8.2 (nginx reports `X-Forwarded-Proto: http` inbound); the same request now signs in cleanly while cross-site origins still 403.
## [0.8.2] - 2026-09-28
### Fixed
- **Self-hosted sign-up works on any origin** — trusted origins are now derived from the request's public host (forwarded by nginx), so browsing the instance via a LAN IP, hostname, or custom port no longer fails every auth POST with a 403. Static `VITE_APP_URL`/`AUTH_BASE_URL` origins and `BETTER_AUTH_TRUSTED_ORIGINS` still apply.
- **Sign-up/sign-in failures are surfaced, not swallowed** — better-auth resolves `{ data, error }` instead of throwing, so a failed request previously showed "Account created — signing you in" and bounced back to the bootstrap screen. The real server error (invalid origin, rate limit, disabled sign-up) is now displayed.
- **Mail probe can no longer break auth** — `hasMailTransport` degrades to "no transport" instead of throwing when `core.settings` is unreadable, and the no-transport startup backfill is best-effort. Previously, auth could crash-loop while the backend was still migrating on a fresh volume — every sign-up 502'd during the window.
## [0.8.1] - 2026-09-28
### Fixed
- **SMTP-less instances no longer involve email at all** — when no mail transport is configured (no Resend key, no `SMTP_HOST`), new accounts are created `emailVerified=true`, the verification send is skipped entirely, and existing unverified users are marked verified on startup. `.env.example` no longer defaults `SMTP_HOST` to the dev-only `mailpit` service, which left production deploys pointing at a transport that does not exist.
## [0.8.0] - 2026-09-28
### Added
+3 -1
View File
@@ -77,7 +77,9 @@ Prerequisites: Docker with the Compose plugin.
curl -fsSL https://raw.githubusercontent.com/Dvorinka/Primora/master/install.sh | bash
```
Installs into `./primora`, generates secrets, and starts the stack on port 80.
Installs into `./primora`, asks one question — the URL you'll open the app on
(domain, IP, optional port) — generates secrets, and starts the stack.
Non-interactive: `PUBLIC_URL=https://primora.example.com bash install.sh`.
Or from a clone:
```bash
+2 -1
View File
@@ -5,7 +5,7 @@ import { requestId } from "hono/request-id";
import { HTTPException } from "hono/http-exception";
import { createClient } from "redis";
import { auth, promoteAdminEmails, runAuthMigrations } from "./lib/auth.js";
import { auth, promoteAdminEmails, runAuthMigrations, verifyUsersWithoutMailTransport } from "./lib/auth.js";
import { authPool } from "./lib/db.js";
import { env } from "./lib/env.js";
@@ -20,6 +20,7 @@ try {
}
await retry("auth_migrations", runAuthMigrations);
await verifyUsersWithoutMailTransport();
await promoteAdminEmails();
const app = new Hono();
+52 -14
View File
@@ -4,7 +4,7 @@ import { admin, jwt } from "better-auth/plugins";
import { getMigrations } from "better-auth/db/migration";
import { authPool } from "./db.js";
import { sendTransactionalEmail } from "./mail.js";
import { hasMailTransport, sendTransactionalEmail } from "./mail.js";
import { getBoolSetting } from "./settings.js";
import { env } from "./env.js";
@@ -53,7 +53,22 @@ export const auth = betterAuth({
appName: "Primora",
secret: env.BETTER_AUTH_SECRET,
baseURL: env.BETTER_AUTH_URL,
trustedOrigins: [env.VITE_APP_URL, env.AUTH_BASE_URL],
// Self-host: the browser reaches /auth through the same nginx origin as the
// app, so whatever host actually served this request is trusted — static
// localhost defaults alone 403 every sign-up on LAN IPs or custom ports.
// An attacker's cross-site Origin still won't match the server host.
trustedOrigins: (request) => {
const origins = [env.VITE_APP_URL, env.AUTH_BASE_URL];
const host = request?.headers.get("x-forwarded-host") ?? request?.headers.get("host");
if (host) {
const proto = request?.headers.get("x-forwarded-proto") ?? "http";
origins.push(`${proto}://${host}`);
// TLS often terminates at an external proxy — nginx then reports http.
// Both schemes on the serving host are legitimately this instance.
origins.push(proto === "https" ? `http://${host}` : `https://${host}`);
}
return origins;
},
database: authPool,
emailAndPassword: {
enabled: true,
@@ -72,20 +87,29 @@ export const auth = betterAuth({
// Gates only the public sign-up endpoint. Users created through the
// admin plugin or OAuth callbacks take different paths and pass.
async before(user, ctx) {
if (ctx?.path !== "/sign-up/email") return;
const { rows } = await authPool.query<{ n: number }>(
`select count(*)::int as n from "user"`,
);
if (rows[0].n === 0) {
// Bootstrap: the first public signup becomes the instance admin.
return { data: { ...user, role: "admin" } };
const data = { ...user };
// Without a mail transport the verification mail can never arrive —
// mark every created account verified so nothing downstream nags
// "unverified" on users nobody can verify.
if (!(await hasMailTransport())) {
data.emailVerified = true;
}
const enabled = await getBoolSetting("auth.signup_enabled", env.SIGNUP_ENABLED);
if (!enabled) {
throw new APIError("FORBIDDEN", {
message: "Sign-up is disabled on this instance.",
});
if (ctx?.path === "/sign-up/email") {
const { rows } = await authPool.query<{ n: number }>(
`select count(*)::int as n from "user"`,
);
if (rows[0].n === 0) {
// Bootstrap: the first public signup becomes the instance admin.
return { data: { ...data, role: "admin" } };
}
const enabled = await getBoolSetting("auth.signup_enabled", env.SIGNUP_ENABLED);
if (!enabled) {
throw new APIError("FORBIDDEN", {
message: "Sign-up is disabled on this instance.",
});
}
}
return data.emailVerified === user.emailVerified ? undefined : { data };
},
},
},
@@ -94,6 +118,9 @@ export const auth = betterAuth({
sendOnSignUp: true,
autoSignInAfterVerification: true,
async sendVerificationEmail({ user, url }) {
// No transport at all → the mail can never arrive; the user was already
// marked verified at creation.
if (!(await hasMailTransport())) return;
// Verification is optional — a misconfigured transport must not fail
// sign-up or sign-in.
try {
@@ -143,6 +170,17 @@ export async function runAuthMigrations() {
await migrations.runMigrations();
}
/** No transport → existing unverified users can never resolve that state.
* Best-effort: a startup failure here must never take the service down. */
export async function verifyUsersWithoutMailTransport() {
try {
if (await hasMailTransport()) return;
await authPool.query(`update "user" set "emailVerified" = true where "emailVerified" = false`);
} catch (error) {
console.warn(JSON.stringify({ level: "warn", msg: "verify_without_transport_failed", error }));
}
}
/** Bootstrap admins by email — run after migrations so the role column exists. */
export async function promoteAdminEmails() {
const emails = (env.AUTH_ADMIN_EMAILS ?? "")
+15
View File
@@ -30,6 +30,21 @@ async function resolveMailConfig() {
} as const;
}
// True when a mail transport exists (Resend key or SMTP host). Verification
// and reset mails only make sense when mail can actually leave the box.
// Mail is optional — a failed settings read must degrade to "no transport",
// never break sign-up.
export async function hasMailTransport(): Promise<boolean> {
try {
const cfg = await resolveMailConfig();
if ("resendApiKey" in cfg && cfg.resendApiKey) return true;
return "smtp" in cfg && Boolean(cfg.smtp);
} catch (error) {
console.warn(JSON.stringify({ level: "warn", msg: "mail_transport_probe_failed", error }));
return false;
}
}
export async function sendTransactionalEmail(input: {
to: string;
subject: string;
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@primora/desktop",
"version": "0.8.0",
"version": "0.8.3",
"private": true,
"description": "Primora desktop shell — thin Tauri client for a self-hosted deployment",
"scripts": {
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Primora",
"version": "0.8.0",
"version": "0.8.3",
"identifier": "dev.tdvorak.primora",
"build": {
"frontendDist": "../ui"
+13 -2
View File
@@ -735,11 +735,22 @@ export default function App() {
await runPending("auth-submit", async () => {
setAuthMessage("");
try {
// better-auth client resolves { data, error } instead of throwing on
// HTTP failures — surface res.error or the UI claims success while
// nothing was created.
if (mode() === "sign-up") {
await authClient.signUp.email({ email: email(), password: password(), name: name() });
const res = await authClient.signUp.email({ email: email(), password: password(), name: name() });
if (res.error) {
setAuthMessage(res.error.message ?? "Sign-up failed");
return;
}
setAuthMessage("Account created — signing you in.");
} else {
await authClient.signIn.email({ email: email(), password: password() });
const res = await authClient.signIn.email({ email: email(), password: password() });
if (res.error) {
setAuthMessage(res.error.message ?? "Sign-in failed");
return;
}
}
} catch (error) {
setAuthMessage(getErrorMessage(error, "Authentication failed"));
+69 -14
View File
@@ -3,8 +3,14 @@
#
# curl -fsSL https://raw.githubusercontent.com/Dvorinka/Primora/master/install.sh | bash
#
# Installs into ./primora (override with PRIMORA_DIR). Non-interactive:
# DOMAIN=example.com NGINX_PORT=8080 bash install.sh
# Installs into ./primora (override with PRIMORA_DIR). On a fresh install,
# interactive shells get one question — the URL the app will be opened on
# (domain or IP, optional port). Non-interactive:
# PUBLIC_URL=https://primora.example.com bash install.sh
# DOMAIN=192.168.1.50 NGINX_PORT=8085 bash install.sh (legacy form)
#
# https URLs mean TLS terminates at your own proxy in front of Primora's
# bundled nginx (which always speaks http on NGINX_PORT).
#
# Pulls prebuilt GHCR images when available; otherwise downloads the source
# tarball and builds locally. Either way the result is the same compose stack.
@@ -14,8 +20,8 @@ set -euo pipefail
REPO="Dvorinka/Primora"
REF="${PRIMORA_REF:-master}"
DIR="${PRIMORA_DIR:-primora}"
DOMAIN="${DOMAIN:-localhost}"
NGINX_PORT="${NGINX_PORT:-80}"
PUBLIC_URL="${PUBLIC_URL:-}"
info() { echo "==> $*"; }
die() { echo "ERROR: $*" >&2; exit 1; }
@@ -37,7 +43,52 @@ for f in docker-compose.yml .env.example infra/nginx/default.conf; do
curl -fsSL "https://raw.githubusercontent.com/$REPO/$REF/$f" -o "$f"
done
if [ ! -f .env ]; then
if [ -f .env ]; then
info "Existing .env kept"
# Display URL comes from the file — answers here would change nothing.
PUBLIC_URL="$(grep -E '^VITE_APP_URL=' .env | tail -1 | cut -d= -f2- || true)"
[ -z "$PUBLIC_URL" ] && PUBLIC_URL="http://localhost"
else
# Public URL — the single question. Env wins, then prompt, then default.
if [ -z "$PUBLIC_URL" ] && [ -n "${DOMAIN:-}" ]; then
PUBLIC_URL="http://${DOMAIN}"
[ "$NGINX_PORT" != "80" ] && PUBLIC_URL="$PUBLIC_URL:$NGINX_PORT"
fi
if [ -z "$PUBLIC_URL" ]; then
DEFAULT_URL="http://localhost"
[ "$NGINX_PORT" != "80" ] && DEFAULT_URL="http://localhost:$NGINX_PORT"
# Under `curl | bash` stdin is the script pipe — prompt on the
# controlling terminal instead. No tty (CI/cron) → default silently.
if exec 9</dev/tty 2>/dev/null; then
read -rp "URL where you'll open Primora (domain or IP, e.g. https://primora.example.com) [$DEFAULT_URL]: " PUBLIC_URL <&9 || true
exec 9>&-
fi
PUBLIC_URL="${PUBLIC_URL:-$DEFAULT_URL}"
fi
# Normalize: default scheme http, strip trailing slash.
case "$PUBLIC_URL" in
http://*|https://*) ;;
*) PUBLIC_URL="http://$PUBLIC_URL" ;;
esac
PUBLIC_URL="${PUBLIC_URL%/}"
# Split host[:port]; URL paths aren't supported — nginx mounts auth at /auth.
REST="${PUBLIC_URL#*://}"
HOSTPORT="${REST%%/*}"
[ "$REST" != "$HOSTPORT" ] && die "URL paths are not supported — use a domain or port instead: $PUBLIC_URL"
if [[ "$HOSTPORT" == *:* ]]; then
HOST="${HOSTPORT%%:*}"
PORT="${HOSTPORT##*:}"
else
HOST="$HOSTPORT"; PORT=""
fi
[ -z "$HOST" ] && die "could not parse a host from: $PUBLIC_URL"
if [ -n "$PORT" ]; then
[[ "$PORT" =~ ^[0-9]+$ ]] || die "invalid port in: $PUBLIC_URL"
NGINX_PORT="$PORT"
fi
info "Generating .env with random secrets"
cp .env.example .env
@@ -53,12 +104,15 @@ if [ ! -f .env ]; then
"${SED[@]}" "s|^PRIMORA_ENCRYPTION_KEY=.*|PRIMORA_ENCRYPTION_KEY=$ENCRYPTION_KEY|" .env
"${SED[@]}" "s|^NGINX_PORT=.*|NGINX_PORT=$NGINX_PORT|" .env
if [ "$DOMAIN" != "localhost" ]; then
"${SED[@]}" "s|http://localhost|http://$DOMAIN|g" .env
"${SED[@]}" "s|^COOKIE_DOMAIN=.*|COOKIE_DOMAIN=$DOMAIN|" .env
# Every public URL var (VITE_APP_URL, BETTER_AUTH_URL, AUTH_BASE_URL,
# VITE_AUTH_BASE_URL, VITE_API_BASE_URL, BACKEND_PUBLIC_URL) is built on the
# http://localhost prefix in .env.example — one rewrite covers them all.
"${SED[@]}" "s|http://localhost|$PUBLIC_URL|g" .env
# Cookie domains only apply to real DNS names — never IPs or localhost.
if [ "$HOST" != "localhost" ] && ! [[ "$HOST" =~ ^[0-9.]+$ ]]; then
"${SED[@]}" "s|^COOKIE_DOMAIN=.*|COOKIE_DOMAIN=$HOST|" .env
fi
else
info "Existing .env kept"
fi
# Compose v5 skips `pull` for services that declare build:, so fetch the
@@ -79,11 +133,12 @@ else
docker compose up -d --build
fi
DISPLAY_URL="http://$DOMAIN"
[ "$NGINX_PORT" != "80" ] && DISPLAY_URL="$DISPLAY_URL:$NGINX_PORT"
info "Primora is starting"
echo " Dashboard: $DISPLAY_URL"
echo " Health: $DISPLAY_URL/api/v1/health/liveness"
echo " Dashboard: $PUBLIC_URL"
echo " Health: $PUBLIC_URL/api/v1/health/liveness"
echo " Logs: cd $DIR && docker compose logs -f"
echo " Stop: cd $DIR && docker compose down"
if [[ "$PUBLIC_URL" == https://* ]]; then
echo
echo " Primora listens on http port $NGINX_PORT — point your TLS proxy there."
fi
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "primora",
"version": "0.8.0",
"version": "0.8.3",
"private": true,
"packageManager": "[email protected]",
"workspaces": [