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:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 [
|
||||
{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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("/");
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user