···168168// --- Login ---
169169170170async function login(handle: string): Promise<void> {
171171- // Remember where to send the user after the OAuth round-trip, but
172172- // never back to /login or /oauth/callback (that would loop).
171171+ // Remember where to send the user after the OAuth round-trip, but never
172172+ // back to /oauth/callback (that would loop).
173173 try {
174174 const here = window.location.pathname;
175175- const dest = here === "/login" || here.startsWith("/oauth/") ? "/" : here;
175175+ const dest = here.startsWith("/oauth/") ? "/" : here;
176176 sessionStorage.setItem(POST_LOGIN_KEY, dest);
177177 } catch {
178178 // non-fatal
+18
web/src/lib/loginModal.tsx
···22 createContext,
33 useCallback,
44 useContext,
55+ useEffect,
56 useState,
67 type ReactNode,
78} from "react";
99+import { useLocation, useNavigate } from "react-router-dom";
810911interface LoginModalCtx {
1012 open: boolean;
···1820 const [open, setOpen] = useState(false);
1921 const openLogin = useCallback(() => setOpen(true), []);
2022 const closeLogin = useCallback(() => setOpen(false), []);
2323+2424+ // Open the modal when we land on a URL with ?login=1 (auth-required loader
2525+ // redirects use this), then strip the param so refreshes don't re-trigger.
2626+ const location = useLocation();
2727+ const navigate = useNavigate();
2828+ useEffect(() => {
2929+ const params = new URLSearchParams(location.search);
3030+ if (params.get("login") !== "1") return;
3131+ setOpen(true);
3232+ params.delete("login");
3333+ const remaining = params.toString();
3434+ navigate(location.pathname + (remaining ? `?${remaining}` : ""), {
3535+ replace: true,
3636+ });
3737+ }, [location.pathname, location.search, navigate]);
3838+2139 return (
2240 <LoginModalContext.Provider value={{ open, openLogin, closeLogin }}>
2341 {children}
-61
web/src/pages/Login.tsx
···11-import { MessageSquare, Pin, User, Monitor } from "lucide-react";
22-import { usePageTitle } from "../hooks/usePageTitle";
33-import LoginForm from "../components/form/LoginForm";
44-55-export default function Login() {
66- usePageTitle("Login — atbbs");
77-88- return (
99- <div className="h-full flex flex-col justify-center overflow-hidden">
1010- <div className="text-center mb-8">
1111- <picture>
1212- <source
1313- srcSet="/hero-dark.svg"
1414- media="(prefers-color-scheme: dark)"
1515- />
1616- <img
1717- src="/hero.svg"
1818- alt="@bbs"
1919- className="mx-auto mb-4"
2020- style={{ width: 140, imageRendering: "pixelated" }}
2121- />
2222- </picture>
2323- <p className="text-neutral-400">
2424- Use any{" "}
2525- <a
2626- href="https://atproto.com"
2727- className="text-neutral-400 hover:text-neutral-300 underline underline-offset-2"
2828- >
2929- AT Protocol
3030- </a>{" "}
3131- account.
3232- </p>
3333- </div>
3434-3535- <div className="mb-6">
3636- <LoginForm />
3737- </div>
3838-3939- <div className="bg-neutral-900 border border-neutral-800 rounded p-4 text-xs text-neutral-400 space-y-3">
4040- <p>Once signed in, you can:</p>
4141- <ul className="space-y-2">
4242- <li className="flex items-center gap-2">
4343- <MessageSquare size={14} /> Post threads and replies
4444- </li>
4545- <li className="flex items-center gap-2">
4646- <Pin size={14} /> Pin boards you like
4747- </li>
4848- <li className="flex items-center gap-2">
4949- <User size={14} /> Set up a profile
5050- </li>
5151- <li className="flex items-center gap-2">
5252- <Monitor size={14} /> Start your own community
5353- </li>
5454- </ul>
5555- <p className="text-neutral-400 pt-3 border-t border-neutral-800">
5656- We'll redirect you to your provider to continue.
5757- </p>
5858- </div>
5959- </div>
6060- );
6161-}
+1-1
web/src/router/loaders/auth.ts
···44export async function requireAuth() {
55 await ensureAuthReady();
66 const user = getCurrentUser();
77- if (!user) throw redirect("/login");
77+ if (!user) throw redirect("/?login=1");
88 return user;
99}
-2
web/src/router/routes.tsx
···99import ErrorPage from "../components/ErrorPage";
10101111import Home from "../pages/Home";
1212-import Login from "../pages/Login";
1312import OAuthCallback from "../pages/OAuthCallback";
1413import Profile from "../pages/Profile";
1514import BBS from "../pages/BBS";
···3837 errorElement: <ErrorPage />,
3938 children: [
4039 { path: "/", loader: homeLoader, element: <Home /> },
4141- { path: "/login", element: <Login /> },
4240 { path: "/oauth/callback", element: <OAuthCallback /> },
4341 { path: "/account", loader: () => redirect("/") },
4442 {