Commit4b66018unknown_key
fix: bulletproof post-import redirect + mount orphan /legal/* sub-routes
fix: bulletproof post-import redirect + mount orphan /legal/* sub-routes
Two real 404 sources reported by users and confirmed by audit:
1. GitHub import post-success redirect (src/routes/import.tsx):
- After importSingleRepo() the redirect path was rebuilt from a
shadowed local 'targetName' that was a second sanitizeRepoName()
call instead of the actually-written DB row. If sanitization
ever drifted (or the DB insert silently dropped) the user
landed on /<user>/<wrong-name> → requireRepoAccess 404.
- Now we read the row back and use storedRepo.name. If it isn't
there (insert failed) we redirect to /import with a clear error
instead of dumping the user on a 404 page.
- Also harden the GitHub response handling: surface res.json()
.message in the error string ('Bad credentials', 'Not Found',
rate-limit text) instead of just the status code, and catch
non-JSON response bodies cleanly.
2. /legal/dmca, /legal/terms, /legal/privacy, /legal/acceptable-use
files existed under src/routes/legal/ but were never mounted in
app.tsx — only the short paths (/terms, /privacy, /acceptable-use)
from the parent legal.tsx were live. Every cross-link within the
long-form legal pages (4 instances of /legal/dmca alone) 404'd.
All four /legal/* routes now return 200 (verified). Import flow now
either succeeds or surfaces a real diagnosable error to the user.2 files changed+76−274b66018538e8379bfc8f6f2821c6134f2b0986e7
2 changed files+76−27
Modifiedsrc/app.tsx+12−0View fileUnifiedSplit
@@ -47,6 +47,10 @@ import demoRoutes from "./routes/demo";
4747import insightRoutes from "./routes/insights";
4848import dashboardRoutes from "./routes/dashboard";
4949import legalRoutes from "./routes/legal";
50import legalDmcaRoutes from "./routes/legal/dmca";
51import legalTermsRoutes from "./routes/legal/terms";
52import legalPrivacyRoutes from "./routes/legal/privacy";
53import legalAcceptableUseRoutes from "./routes/legal/acceptable-use";
5054import importRoutes from "./routes/import";
5155import importBulkRoutes from "./routes/import-bulk";
5256import importSecretsRoutes from "./routes/import-secrets";
@@ -381,6 +385,14 @@ app.route("/", dashboardRoutes);
381385
382386// Legal pages (terms, privacy, AUP)
383387app.route("/", legalRoutes);
388// Long-form legal sub-pages — /legal/{terms,privacy,acceptable-use,dmca}.
389// The main `legal.tsx` serves the short canonical paths (/terms, /privacy,
390// /acceptable-use); these are the formal versions that the legal pages
391// internally link to each other.
392app.route("/", legalTermsRoutes);
393app.route("/", legalPrivacyRoutes);
394app.route("/", legalAcceptableUseRoutes);
395app.route("/", legalDmcaRoutes);
384396
385397// GitHub import / migration
386398app.route("/", importRoutes);
Modifiedsrc/routes/import.tsx+64−27View fileUnifiedSplit
@@ -831,14 +831,33 @@ importRoutes.post("/import/github/repo", requireAuth, requireAdmin, async (c) =>
831831 );
832832
833833 if (!res.ok) {
834 // Try to pull GitHub's actual error message — "Bad credentials",
835 // "Not Found", or rate-limit text is far more useful than a bare code.
836 let detail = "";
837 try {
838 const body = await res.json();
839 detail = body?.message ? ` — ${String(body.message)}` : "";
840 } catch {
841 /* non-JSON body, skip */
842 }
834843 return c.redirect(
835844 `/import?error=${encodeURIComponent(
836 `Repository not found on GitHub (${res.status}). Check the URL and that it's public.`
845 `GitHub said ${res.status}${detail}. Check the URL and that the repo is public (or supply a token).`
837846 )}`
838847 );
839848 }
840849
841 const ghRepoData: GitHubRepo = await res.json();
850 let ghRepoData: GitHubRepo;
851 try {
852 ghRepoData = (await res.json()) as GitHubRepo;
853 } catch (err) {
854 console.error("[import] non-JSON response from GitHub:", err);
855 return c.redirect(
856 `/import?error=${encodeURIComponent(
857 "GitHub returned a response we couldn't parse. Try again in a moment."
858 )}`
859 );
860 }
842861
843862 await importSingleRepo(user, ghRepoData, optionalToken);
844863
@@ -849,34 +868,52 @@ importRoutes.post("/import/github/repo", requireAuth, requireAdmin, async (c) =>
849868 // checklist. Fire-and-forget semantics: any failure (no token, network
850869 // error, no secrets, DB blip) collapses to "skip the checklist step"
851870 // and we redirect straight to the repo home.
852 const targetName = sanitizeRepoName(ghRepoData.name);
871 //
872 // CRITICAL — never reconstruct the redirect path from local strings:
873 // sanitizeRepoName/case/etc could differ from what importSingleRepo
874 // actually wrote. We had a post-import 404 in production because the
875 // shadowed targetName drifted from the DB row. Read the row back.
876 const [storedRepo] = await db
877 .select({ id: repositories.id, name: repositories.name })
878 .from(repositories)
879 .where(
880 and(
881 eq(repositories.ownerId, user.id),
882 eq(repositories.name, sanitizeRepoName(ghRepoData.name))
883 )
884 )
885 .limit(1);
886
887 if (!storedRepo) {
888 // The clone succeeded but the insert apparently didn't — fail loud
889 // instead of redirecting the user into a 404.
890 console.error(
891 "[import] repo missing after import — owner=" + user.username +
892 " ghRepo=" + ghRepoData.full_name
893 );
894 return c.redirect(
895 `/import?error=${encodeURIComponent(
896 "Import partially completed — please refresh and try again."
897 )}`
898 );
899 }
900
901 const targetName = storedRepo.name;
853902 let secretsRedirect: string | null = null;
854903 if (optionalToken) {
855904 try {
856 const [newRepoRow] = await db
857 .select({ id: repositories.id })
858 .from(repositories)
859 .where(
860 and(
861 eq(repositories.ownerId, user.id),
862 eq(repositories.name, targetName)
863 )
864 )
865 .limit(1);
866 if (newRepoRow) {
867 const { importSecretsForRepo } = await import(
868 "../lib/github-secrets-import"
869 );
870 const result = await importSecretsForRepo({
871 githubOwner: ghOwner,
872 githubRepo: ghRepo,
873 githubToken: optionalToken,
874 gluecronRepositoryId: newRepoRow.id,
875 importedByUserId: user.id,
876 });
877 if (result.imported.length > 0) {
878 secretsRedirect = `/${user.username}/${targetName}/import/secrets`;
879 }
905 const { importSecretsForRepo } = await import(
906 "../lib/github-secrets-import"
907 );
908 const result = await importSecretsForRepo({
909 githubOwner: ghOwner,
910 githubRepo: ghRepo,
911 githubToken: optionalToken,
912 gluecronRepositoryId: storedRepo.id,
913 importedByUserId: user.id,
914 });
915 if (result.imported.length > 0) {
916 secretsRedirect = `/${user.username}/${targetName}/import/secrets`;
880917 }
881918 } catch (err) {
882919 console.error("[import] secrets migration skipped:", err);
883920