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
|
secret = settings.auth_jwt_secret
|
||||||
if not secret:
|
if not secret:
|
||||||
return None
|
return None
|
||||||
|
# Accept pre-rebrand issuer so existing sessions keep working.
|
||||||
|
for issuer in ("periscope-local", "pinscope-local"):
|
||||||
try:
|
try:
|
||||||
return jwt.decode(
|
return jwt.decode(
|
||||||
token,
|
token,
|
||||||
secret,
|
secret,
|
||||||
algorithms=[ALGORITHM],
|
algorithms=[ALGORITHM],
|
||||||
issuer="periscope-local",
|
issuer=issuer,
|
||||||
options={"verify_aud": False},
|
options={"verify_aud": False},
|
||||||
leeway=10,
|
leeway=10,
|
||||||
)
|
)
|
||||||
except jwt.PyJWTError:
|
except jwt.PyJWTError:
|
||||||
|
continue
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
What's new in Periscope.
|
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
|
## 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.
|
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: "" },
|
canvas: { browser: "" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{ source: "/login", destination: "/sign-in", permanent: false },
|
||||||
|
{ source: "/register", destination: "/sign-up", permanent: false },
|
||||||
|
];
|
||||||
|
},
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -44,12 +44,12 @@ export function Nav() {
|
|||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link href="/login">
|
<Link href="/sign-in">
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/login">
|
<Link href="/sign-up">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
|
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ export function NavAuthCluster() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link href="/login">
|
<Link href="/sign-in">
|
||||||
<Button variant="ghost" size="sm">
|
<Button variant="ghost" size="sm">
|
||||||
Sign in
|
Sign in
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/login">
|
<Link href="/sign-up">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
|
className="bg-blue-600 hover:bg-blue-500 text-white border-0"
|
||||||
@@ -41,7 +41,7 @@ export function NavAuthCluster() {
|
|||||||
|
|
||||||
export function PrimaryCta() {
|
export function PrimaryCta() {
|
||||||
const { isSignedIn } = useOptionalAuth();
|
const { isSignedIn } = useOptionalAuth();
|
||||||
const href = isSignedIn ? "/dashboard" : "/login";
|
const href = isSignedIn ? "/dashboard" : "/sign-up";
|
||||||
const label = isSignedIn ? "Go to Dashboard" : "Start for free";
|
const label = isSignedIn ? "Go to Dashboard" : "Start for free";
|
||||||
return (
|
return (
|
||||||
<Link href={href}>
|
<Link href={href}>
|
||||||
@@ -59,7 +59,7 @@ export function PrimaryCta() {
|
|||||||
export function PricingCta() {
|
export function PricingCta() {
|
||||||
const { isSignedIn } = useOptionalAuth();
|
const { isSignedIn } = useOptionalAuth();
|
||||||
return (
|
return (
|
||||||
<Link href={isSignedIn ? "/billing" : "/login"}>
|
<Link href={isSignedIn ? "/billing" : "/sign-up"}>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-blue-600 hover:bg-blue-500 text-white border-0 gap-1.5"
|
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(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
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);
|
storeAuthToken(data.token);
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
|
|||||||
@@ -35,9 +35,13 @@ export default function SignUpPage() {
|
|||||||
const data = await res.json().catch(() => ({}));
|
const data = await res.json().catch(() => ({}));
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const detail = data.detail;
|
const detail = data.detail;
|
||||||
throw new Error(
|
const msg =
|
||||||
typeof detail === "string" ? detail : "Could not create account",
|
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);
|
storeAuthToken(data.token);
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ fi
|
|||||||
|
|
||||||
log "Ensuring public URL in .env ($SITE)"
|
log "Ensuring public URL in .env ($SITE)"
|
||||||
upsert_env NEXT_PUBLIC_API_URL "$SITE" .env
|
upsert_env NEXT_PUBLIC_API_URL "$SITE" .env
|
||||||
# JSON list — keep it a single line so docker compose / pydantic-settings parse it.
|
# Allow both Periscope and legacy Pinscope host during transition.
|
||||||
upsert_env CORS_ORIGINS "[\"$SITE\"]" .env
|
upsert_env CORS_ORIGINS "[\"$SITE\",\"https://pinscope.michelebigi.it\"]" .env
|
||||||
|
|
||||||
KEY="$(read_env DEEPSEEK_API_KEY .env || true)"
|
KEY="$(read_env DEEPSEEK_API_KEY .env || true)"
|
||||||
if [[ -z "$KEY" || "$KEY" == "sk-..." ]]; then
|
if [[ -z "$KEY" || "$KEY" == "sk-..." ]]; then
|
||||||
|
|||||||
Reference in New Issue
Block a user