mirror of
https://github.com/Dvorinka/Primora.git
synced 2026-10-28 15:51:23 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49ca9067a9 | ||
|
|
805a58e32f | ||
|
|
d09b0a1c2d | ||
|
|
92b1810c07 | ||
|
|
3ad37c190b | ||
|
|
cb56cedb1f |
No files matched your search
@@ -339,6 +339,8 @@ jobs:
|
|||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# anchore/sbom-action does not create the output directory.
|
||||||
|
- run: mkdir -p dist-release
|
||||||
- uses: anchore/sbom-action@v0
|
- uses: anchore/sbom-action@v0
|
||||||
with:
|
with:
|
||||||
image: ghcr.io/${{ env.GHCR_OWNER }}/primora-backend:${{ needs.verify.outputs.version }}
|
image: ghcr.io/${{ env.GHCR_OWNER }}/primora-backend:${{ needs.verify.outputs.version }}
|
||||||
|
|||||||
@@ -4,6 +4,14 @@ All notable changes to Primora. Format follows [Keep a Changelog](https://keepac
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [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
|
## [0.8.1] - 2026-09-28
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -53,7 +53,19 @@ export const auth = betterAuth({
|
|||||||
appName: "Primora",
|
appName: "Primora",
|
||||||
secret: env.BETTER_AUTH_SECRET,
|
secret: env.BETTER_AUTH_SECRET,
|
||||||
baseURL: env.BETTER_AUTH_URL,
|
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}`);
|
||||||
|
}
|
||||||
|
return origins;
|
||||||
|
},
|
||||||
database: authPool,
|
database: authPool,
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -155,10 +167,15 @@ export async function runAuthMigrations() {
|
|||||||
await migrations.runMigrations();
|
await migrations.runMigrations();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** No transport → existing unverified users can never resolve that state. */
|
/** 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() {
|
export async function verifyUsersWithoutMailTransport() {
|
||||||
if (await hasMailTransport()) return;
|
try {
|
||||||
await authPool.query(`update "user" set "emailVerified" = true where "emailVerified" = false`);
|
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. */
|
/** Bootstrap admins by email — run after migrations so the role column exists. */
|
||||||
|
|||||||
@@ -32,10 +32,17 @@ async function resolveMailConfig() {
|
|||||||
|
|
||||||
// True when a mail transport exists (Resend key or SMTP host). Verification
|
// 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.
|
// 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> {
|
export async function hasMailTransport(): Promise<boolean> {
|
||||||
const cfg = await resolveMailConfig();
|
try {
|
||||||
if ("resendApiKey" in cfg && cfg.resendApiKey) return true;
|
const cfg = await resolveMailConfig();
|
||||||
return "smtp" in cfg && Boolean(cfg.smtp);
|
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: {
|
export async function sendTransactionalEmail(input: {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@primora/desktop",
|
"name": "@primora/desktop",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Primora desktop shell — thin Tauri client for a self-hosted deployment",
|
"description": "Primora desktop shell — thin Tauri client for a self-hosted deployment",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Primora",
|
"productName": "Primora",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"identifier": "dev.tdvorak.primora",
|
"identifier": "dev.tdvorak.primora",
|
||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../ui"
|
"frontendDist": "../ui"
|
||||||
|
|||||||
@@ -735,11 +735,22 @@ export default function App() {
|
|||||||
await runPending("auth-submit", async () => {
|
await runPending("auth-submit", async () => {
|
||||||
setAuthMessage("");
|
setAuthMessage("");
|
||||||
try {
|
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") {
|
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.");
|
setAuthMessage("Account created — signing you in.");
|
||||||
} else {
|
} 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) {
|
} catch (error) {
|
||||||
setAuthMessage(getErrorMessage(error, "Authentication failed"));
|
setAuthMessage(getErrorMessage(error, "Authentication failed"));
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "primora",
|
"name": "primora",
|
||||||
"version": "0.8.1",
|
"version": "0.8.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "[email protected]",
|
"packageManager": "[email protected]",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
|
|||||||
Reference in new issue
Block a user