Fix sign-in links and legacy JWT after Periscope rebrand.

/login 404ed from marketing CTAs; redirect to /sign-in and accept pinscope-local tokens plus dual-host CORS so login works on both hostnames.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-13 20:12:53 +02:00
co-authored by Cursor
parent 8d2b85600f
commit dc364f5926
8 changed files with 51 additions and 23 deletions
+14 -11
View File
@@ -32,14 +32,17 @@ def decode_token(token: str) -> dict[str, Any] | None:
secret = settings.auth_jwt_secret
if not secret:
return None
try:
return jwt.decode(
token,
secret,
algorithms=[ALGORITHM],
issuer="periscope-local",
options={"verify_aud": False},
leeway=10,
)
except jwt.PyJWTError:
return None
# Accept pre-rebrand issuer so existing sessions keep working.
for issuer in ("periscope-local", "pinscope-local"):
try:
return jwt.decode(
token,
secret,
algorithms=[ALGORITHM],
issuer=issuer,
options={"verify_aud": False},
leeway=10,
)
except jwt.PyJWTError:
continue
return None
+8
View File
@@ -2,6 +2,14 @@
What's new in Periscope.
## 2.29.1 — 2026-09-13 — Fix sign-in /login 404 after rebrand
Landing CTAs pointed at `/login`, which did not exist (page is `/sign-in`). Added redirects, accepted legacy JWT issuer `pinscope-local`, and CORS for both Periscope and Pinscope hosts.
- [Fixed] `/login``/sign-in`, `/register``/sign-up`; marketing links updated.
- [Fixed] JWT decode accepts `pinscope-local` issuer from pre-rebrand sessions.
- [Fixed] Deploy script CORS includes both public hostnames.
## 2.29.0 — 2026-09-13 — Rebrand to Periscope
Product name, package (`periscopex`), Docker containers, deploy script, and default site URL are now **Periscope** (`https://periscope.michelebigi.it`). Deterministic finding IDs use the `PE-*` prefix. Existing project.json `pinscope_version` and browser storage keys are still read.
+6
View File
@@ -27,6 +27,12 @@ const nextConfig: NextConfig = {
canvas: { browser: "" },
},
},
async redirects() {
return [
{ source: "/login", destination: "/sign-in", permanent: false },
{ source: "/register", destination: "/sign-up", permanent: false },
];
},
async headers() {
return [
{
+2 -2
View File
@@ -44,12 +44,12 @@ export function Nav() {
</Link>
) : (
<>
<Link href="/login">
<Link href="/sign-in">
<Button variant="ghost" size="sm">
Sign in
</Button>
</Link>
<Link href="/login">
<Link href="/sign-up">
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
+4 -4
View File
@@ -22,12 +22,12 @@ export function NavAuthCluster() {
}
return (
<>
<Link href="/login">
<Link href="/sign-in">
<Button variant="ghost" size="sm">
Sign in
</Button>
</Link>
<Link href="/login">
<Link href="/sign-up">
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
@@ -41,7 +41,7 @@ export function NavAuthCluster() {
export function PrimaryCta() {
const { isSignedIn } = useOptionalAuth();
const href = isSignedIn ? "/dashboard" : "/login";
const href = isSignedIn ? "/dashboard" : "/sign-up";
const label = isSignedIn ? "Go to Dashboard" : "Start for free";
return (
<Link href={href}>
@@ -59,7 +59,7 @@ export function PrimaryCta() {
export function PricingCta() {
const { isSignedIn } = useOptionalAuth();
return (
<Link href={isSignedIn ? "/billing" : "/login"}>
<Link href={isSignedIn ? "/billing" : "/sign-up"}>
<Button
size="sm"
className="bg-blue-600 hover:bg-blue-500 text-white border-0 gap-1.5"
@@ -29,7 +29,14 @@ export default function SignInPage() {
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(data.detail || "Sign in failed");
const detail = data.detail;
const msg =
typeof detail === "string"
? detail
: Array.isArray(detail)
? detail.map((d: { msg?: string }) => d.msg || JSON.stringify(d)).join("; ")
: "Sign in failed";
throw new Error(msg);
}
storeAuthToken(data.token);
router.replace("/");
@@ -35,9 +35,13 @@ export default function SignUpPage() {
const data = await res.json().catch(() => ({}));
if (!res.ok) {
const detail = data.detail;
throw new Error(
typeof detail === "string" ? detail : "Could not create account",
);
const msg =
typeof detail === "string"
? detail
: Array.isArray(detail)
? detail.map((d: { msg?: string }) => d.msg || JSON.stringify(d)).join("; ")
: "Could not create account";
throw new Error(msg);
}
storeAuthToken(data.token);
router.replace("/");
+2 -2
View File
@@ -115,8 +115,8 @@ fi
log "Ensuring public URL in .env ($SITE)"
upsert_env NEXT_PUBLIC_API_URL "$SITE" .env
# JSON list — keep it a single line so docker compose / pydantic-settings parse it.
upsert_env CORS_ORIGINS "[\"$SITE\"]" .env
# Allow both Periscope and legacy Pinscope host during transition.
upsert_env CORS_ORIGINS "[\"$SITE\",\"https://pinscope.michelebigi.it\"]" .env
KEY="$(read_env DEEPSEEK_API_KEY .env || true)"
if [[ -z "$KEY" || "$KEY" == "sk-..." ]]; then