Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions install/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -47,20 +47,28 @@ server {
# Web console: a static build served straight off disk. Everything it
# talks to is under /api on this same host, so no CORS is involved.
#
location = /app {
return 301 /app/;
}

# The directory is named to match the URL so plain root resolution
# applies. "alias" with try_files appends the whole URI to the alias
# rather than the remainder, which silently breaks the SPA fallback.
location ^~ /app/ {
include /etc/nginx/mime.types;
root NGINX_DIR/web;
try_files $uri $uri/ /app/index.html;
# index.html names the hashed bundles, so it must be revalidated or a
# browser keeps loading the previous build after a deploy. "expires"
# rather than add_header: any add_header here would stop the server
# block's HSTS header being inherited.
expires -1;
}

location ^~ /app/assets/ {
include /etc/nginx/mime.types;
root NGINX_DIR/web;
expires 30d; # hashed filenames
add_header Cache-Control "public, immutable";
expires max; # hashed filenames
}

location / {
Expand Down
32 changes: 14 additions & 18 deletions web/src/components/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { KeyRound, Loader2 } from "lucide-react";
import { motion } from "motion/react";
import { useEffect, useState } from "react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
Expand All @@ -15,30 +15,26 @@ import { asset } from "@/lib/utils";
* signed link: they are handed straight back to the API, which is what
* checks them. Nothing here can decide whether a link is good.
*/
// Read from the URL rather than the router: the link is built by the
// platform's email template, and lands here under either history mode.
function readResetLink(): { uid: number; expires: number; mac: string } | null {
const raw = window.location.href;
const query = raw.slice(raw.indexOf("?") + 1);
const q = new URLSearchParams(raw.includes("?") ? query : "");
const uid = Number(q.get("uid"));
const expires = Number(q.get("expires"));
const mac = q.get("mac");
return uid && expires && mac ? { uid, expires, mac } : null;
}

export function ResetPassword() {
const [params, setParams] = useState<{
uid: number;
expires: number;
mac: string;
} | null>(null);
const [params] = useState(readResetLink);
const [password, setPassword] = useState("");
const [confirm, setConfirm] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string | null>(null);
const [done, setDone] = useState(false);

// Read from the URL rather than the router: the link is built by the
// platform's email template, and lands here under either history mode.
useEffect(() => {
const raw = window.location.href;
const query = raw.slice(raw.indexOf("?") + 1);
const q = new URLSearchParams(raw.includes("?") ? query : "");
const uid = Number(q.get("uid"));
const expires = Number(q.get("expires"));
const mac = q.get("mac");
if (uid && expires && mac) setParams({ uid, expires, mac });
}, []);

const submit = async (e: React.FormEvent) => {
e.preventDefault();
if (!params) return;
Expand Down
20 changes: 12 additions & 8 deletions web/src/pages/Account.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useQueryClient } from "@tanstack/react-query";
import { ExternalLink, GitBranch, KeyRound, Loader2, UserRound, UserX } from "lucide-react";
import { useEffect, useState } from "react";
import { useState } from "react";

import { Button } from "@/components/ui/button";
import { ConfirmDialog } from "@/components/ui/confirm";
Expand Down Expand Up @@ -61,11 +61,14 @@ function ProfileSection({ name, email }: Readonly<{ name: string; email: string
const [saved, setSaved] = useState(false);

// The query refetches after a save, so pick the server's values back up
// rather than leaving whatever was typed sitting in the inputs.
useEffect(() => {
// rather than leaving whatever was typed sitting in the inputs. Done while
// rendering, not in an effect, so the stale values never paint.
const [synced, setSynced] = useState({ name, email });
if (synced.name !== name || synced.email !== email) {
setSynced({ name, email });
setDraftName(name);
setDraftEmail(email);
}, [name, email]);
}

const emailChanged = draftEmail !== email;
const nameChanged = draftName !== name;
Expand Down Expand Up @@ -279,18 +282,19 @@ function GithubSection() {
{busy && <Loader2 className="animate-spin" />} Disconnect
</Button>
) : (
<a href={data.authorize_url} target="_blank" rel="noreferrer">
<a href="/account/manage" target="_blank" rel="noreferrer">
<Button size="sm" variant="secondary">
<ExternalLink /> Connect to GitHub
</Button>
</a>
)}
{/* The redirect finishes on the classic site, so this page will
not know about it until it is loaded again. */}
{/* GitHub's callback is registered to the classic site and reads
its own sign-in, so the link is made there. This page will not
know about it until it is loaded again. */}
<p className="mt-2 text-[11px] text-faint">
{data.linked
? "Disconnecting only forgets the platform's copy. Withdraw the authorisation itself from your GitHub applications page."
: "GitHub opens in a new tab. Reload this page once you are done there."}
: "Opens your account on the classic site in a new tab, where GitHub is connected. Reload this page once you are done there."}
</p>
</>
)}
Expand Down
8 changes: 7 additions & 1 deletion web/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,13 @@ const routeTree = rootRoute.addChildren([
adminRoute,
]);

export const router = createRouter({ routeTree });
// Served under /app/ in production, so routes are matched below that
// prefix. A relative base (the demo build) is served from the root.
const base = import.meta.env.BASE_URL;
export const router = createRouter({
routeTree,
basepath: base.startsWith("/") ? base : "/",
});

declare module "@tanstack/react-router" {
interface Register {
Expand Down
Loading