GitFOSS
feat(auth): implement auth-then-redirect in a global way
+ 190
- 110
app/controllers/auth/getLoginView.ts
@@ -1,9 +1,23 @@
 // 3rd-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import LoginView, { LoginViewProps } from "../../views/auth/LoginView";
 
 const getLoginView: ReqHandler = (request, reply) => {
+  const { after_login_goto } =
+    request.query as AppRouteParams[AppRoute.AUTH_LOGIN]["querystring"];
+
+  if (after_login_goto != null && after_login_goto.trim().startsWith("/")) {
+    // ! TODO(security):
+    // ! - [x] check that path is not an external url (avoid open redirect attack)
+    // ! - [ ] check that path belongs to a registered route (how?)
+    // ! - [ ] **do not** honour requests when both conditions ^ are not met
+
+    console.log("after_login_goto:", after_login_goto);
+    request.session.data.auth_redirect_to = after_login_goto;
+  }
+
   const reqHandler = reply.makeRequestHandler(request, reply);
   return reqHandler<LoginViewProps>(LoginView.name, {});
 };

app/controllers/auth/postLoginAction.ts
@@ -1,11 +1,14 @@
 // 3rd-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import LoginView, { LoginViewProps } from "../../views/auth/LoginView";
 import { makeAuthService } from "../../services/auth";
 
 const postLoginAction: ReqHandler = async (request, reply) => {
+  const { email_address: emailAddress, password } =
+    request.body as AppRouteParams[AppRoute.AUTH_LOGIN_ACTION]["body"];
+
   const authService = makeAuthService({
     cryptoService: request.cryptoService,
     request,

...
@@ -13,9 +16,6 @@ const postLoginAction: ReqHandler = async (request, reply) => {
 
   const reqHandler = reply.makeRequestHandler(request, reply);
 
-  const { email_address: emailAddress, password } =
-    request.body as AppRoutesParams[AppRoute.AUTH_LOGIN_ACTION]["body"];
-
   const initialValues = { emailAddress };
 
   if (request.validationError != null) {

...
@@ -30,14 +30,14 @@ const postLoginAction: ReqHandler = async (request, reply) => {
   if (emailAddress.trim() === "") {
     return reqHandler<LoginViewProps>(LoginView.name, {
       errorMessage: "Please provide a non-empty email address.",
-      initialValues: { emailAddress },
+      initialValues,
     });
   }
 
   if (password.trim() === "") {
     return reqHandler<LoginViewProps>(LoginView.name, {
       errorMessage: "Please provide a non-empty password.",
-      initialValues: { emailAddress },
+      initialValues,
     });
   }
 

...
@@ -45,7 +45,7 @@ const postLoginAction: ReqHandler = async (request, reply) => {
     return reqHandler<LoginViewProps>(LoginView.name, {
       errorMessage:
         "Invalid credentials. Please verify your input and try again.",
-      initialValues: { emailAddress },
+      initialValues,
     });
   }
 

...
@@ -58,19 +58,30 @@ const postLoginAction: ReqHandler = async (request, reply) => {
     return reqHandler<LoginViewProps>(LoginView.name, {
       errorMessage:
         "Invalid credentials. Please verify your input and try again.",
-      initialValues: { emailAddress },
+      initialValues,
     });
   }
 
-  const { avatarUri, role, id: userId, username } = user;
+  // Set the session data such as the user is authenticated.
   request.session.data.authenticated = true;
-  request.session.data.curr_user_avatar_uri = avatarUri;
-  request.session.data.curr_user_role = role;
-  request.session.data.curr_user_uid = userId;
-  request.session.data.curr_user_username = username;
+  request.session.data.curr_user_avatar_uri = user.avatarUri;
+  request.session.data.curr_user_role = user.role;
+  request.session.data.curr_user_uid = user.id;
+  request.session.data.curr_user_username = user.username;
 
-  console.log(`Logged user with id: ${userId}`);
+  // Log success
+  console.log(`User (id: ${user.id}, role: ${user.role}) just logged!`);
+
+  // In case a redirect to url is set in session, go there.
+  const authRedirectToUrl = request.session.data.auth_redirect_to;
+  if (authRedirectToUrl != null) {
+    console.log("Will redirect back to before-auth uri.", authRedirectToUrl);
+    request.session.data.auth_redirect_to = null; // prevent replay attack
+    reply.redirect(302, authRedirectToUrl);
+    return reply;
+  }
 
+  // Everything is ok, redirect to user dashboard
   reply.redirect(302, request.namedViewsPathMap[AppRoute.USER_DASHBOARD]);
   return reply;
 };

app/controllers/auth/postRegisterAction.ts
@@ -1,7 +1,7 @@
 // 3rd-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import RegisterView, { RegisterViewProps } from "../../views/auth/RegisterView";
 import { makeAuthService } from "../../services/auth";
 

...
@@ -17,7 +17,7 @@ const postRegisterAction: ReqHandler = async (request, reply) => {
     email_address: emailAddress,
     username,
     password,
-  } = request.body as AppRoutesParams[AppRoute.AUTH_REGISTER_ACTION]["body"];
+  } = request.body as AppRouteParams[AppRoute.AUTH_REGISTER_ACTION]["body"];
 
   const initialValues = { emailAddress, username };
 

app/controllers/organization/getOrganizationDetailsView.ts
@@ -8,7 +8,7 @@ import {
   User,
 } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 // app views

...
@@ -19,7 +19,7 @@ import OrganizationDetailsView, {
 const getOrganizationDetailsView: ReqHandler = async (request, reply) => {
   const { curr_user_uid } = request.session.data;
   const { orgSlug } =
-    request.params as AppRoutesParams[AppRoute.ORGANIZATION_DETAILS]["params"];
+    request.params as AppRouteParams[AppRoute.ORGANIZATION_DETAILS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const organization = (await orgService.getOrganizationBySlug(orgSlug, {

app/controllers/repository/getRepositoryBrowserView.ts
@@ -3,7 +3,7 @@ import type { ReqHandler } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { Const } from "../../const";
 // app services
 import { makeOrganizationService } from "../../services/organization";

...
@@ -19,7 +19,7 @@ import RepositoryDetailsView, {
 
 const getRepositoryBrowserView: ReqHandler = async (request, reply) => {
   const params =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_BROWSER]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_BROWSER]["params"];
   const { orgSlug, repoSlug, currentRef: currRef } = params;
   const path = params["*"] || "";
   const currentRef = currRef || Const.DEFAULT_HEAD_REF;

app/controllers/repository/getRepositoryCommitsLogView.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -14,7 +14,7 @@ import RepositoryCommitsLogView, {
 
 const getRepositoryCommitsLogView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug, currentRef } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_COMMITS_LOG]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_COMMITS_LOG]["params"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

app/controllers/repository/getRepositoryCompareView.ts
@@ -2,7 +2,7 @@
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // generated via script[generate:prisma]
 import { ResourceVisibility } from "@prisma/client";
-import { AppRoute, AppRoutesParams } from "app/routes.defs";
+import { AppRoute, AppRouteParams } from "app/routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -14,7 +14,7 @@ import RepositoryCompareView, {
 
 const getRepositoryCompareView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug, refA, refB } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_COMPARE]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_COMPARE]["params"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

app/controllers/repository/getRepositoryDetailsView.ts
@@ -1,7 +1,7 @@
 // 1st-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { Const } from "../../const";
 // app services
 import { makeOrganizationService } from "../../services/organization";

...
@@ -19,7 +19,7 @@ import {
 
 const getRepositoryDetailsView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_DETAILS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_DETAILS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

app/controllers/repository/getRepositoryForkView.ts
@@ -1,7 +1,7 @@
 // 1st-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -11,7 +11,7 @@ import RepositoryForkView, {
   RepositoryForkViewProps,
 } from "../../views/repository/RepositoryForkView";
 
-type RouteParams = AppRoutesParams[AppRoute.REPOSITORY_FORK];
+type RouteParams = AppRouteParams[AppRoute.REPOSITORY_FORK];
 
 const getRepositoryForkView: ReqHandler = async (request, reply) => {
   if (

app/controllers/repository/getRepositoryShowObjectView.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -14,7 +14,7 @@ import RepositoryShowObjectView, {
 
 const getRepositoryShowObjectView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug, objectId } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_SHOW_OBJECT]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_SHOW_OBJECT]["params"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

app/controllers/repository/postRepositoryCreateAction.ts
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
 // 1st-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { Env } from "../../env";
 // app services
 import { makeRepositoryService } from "../../services/repository";

...
@@ -49,7 +49,7 @@ const postRepositoryCreateAction: ReqHandler = async (request, reply) => {
     repo_short_description: shortDescription,
     repo_visibility: visibility,
     repo_website_url: websiteUrl,
-  } = body as AppRoutesParams[AppRoute.REPOSITORY_CREATE_ACTION]["body"];
+  } = body as AppRouteParams[AppRoute.REPOSITORY_CREATE_ACTION]["body"];
 
   const newRepo = await repoService.createRepository({
     parentOrgSlug, // TODO: Validate it exists first.

app/controllers/repository/postRepositoryForkAction.ts
@@ -3,7 +3,7 @@ import { join, resolve } from "node:path";
 // 1st-party
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { Env } from "../../env";
 // app services
 import { makeOrganizationService } from "../../services/organization";

...
@@ -31,14 +31,14 @@ const postRepositoryForkAction: ReqHandler = async (request, reply) => {
   const { body, params, validationError } = request;
 
   const { orgSlug: sourceOrgSlug, repoSlug: sourceRepoSlug } =
-    params as AppRoutesParams[AppRoute.REPOSITORY_FORK_ACTION]["params"];
+    params as AppRouteParams[AppRoute.REPOSITORY_FORK_ACTION]["params"];
 
   const {
     target_org_slug: targetOrgSlug,
     target_repo_slug: targetRepoSlug,
     target_repo_display_name: targetRepoDisplayName,
     target_repo_visibility: targetRepoVisibility,
-  } = body as AppRoutesParams[AppRoute.REPOSITORY_FORK_ACTION]["body"];
+  } = body as AppRouteParams[AppRoute.REPOSITORY_FORK_ACTION]["body"];
 
   const sourceParentOrg = await organizationService.getOrganizationBySlug(
     sourceOrgSlug

...
@@ -77,7 +77,7 @@ const postRepositoryForkAction: ReqHandler = async (request, reply) => {
       sourceRepo,
       errorMessage,
       initialValues:
-        body as AppRoutesParams[AppRoute.REPOSITORY_FORK_ACTION]["body"],
+        body as AppRouteParams[AppRoute.REPOSITORY_FORK_ACTION]["body"],
     });
   }
 

...
@@ -112,7 +112,7 @@ const postRepositoryForkAction: ReqHandler = async (request, reply) => {
       sourceRepo,
       errorMessage: (err as Error).message,
       initialValues:
-        body as AppRoutesParams[AppRoute.REPOSITORY_FORK_ACTION]["body"],
+        body as AppRouteParams[AppRoute.REPOSITORY_FORK_ACTION]["body"],
     });
   }
 };

app/controllers/repositoryPullRequests/getRepositoryPullRequestCloseAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestCloseAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestCommentCreateAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestCommentCreateAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestCommentDeleteAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestCommentDeleteAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestCommentUpdateAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestCommentUpdateAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestCreateView.ts
@@ -1,9 +1,11 @@
 // 1st-party
+import { Organization } from "@prisma/client";
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import type { RepositoryWithParentAndForkedFromRepos } from "../../types";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { Const } from "../../const";
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { buildRouteLink } from "../../utils/shared";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makeRepositoryService } from "../../services/repository";

...
@@ -18,25 +20,41 @@ import {
 import RepositoryPullRequestCreateView, {
   RepositoryPullRequestCreateViewProps,
 } from "../../views/repositoryPullRequests/RepositoryPullRequestCreateView";
-import { Organization } from "@prisma/client";
 
 const getRepositoryPullRequestCreateView: ReqHandler = async (
   request,
   reply
 ) => {
+  const { from_branch } =
+    request.query as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["querystring"];
+
+  const { orgSlug, repoSlug } =
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["params"];
+
   if (
     request.session.data.authenticated === false ||
     request.session.data.curr_user_uid == null
   ) {
-    reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+    const getBackHereLink = buildRouteLink(
+      AppRoute.REPOSITORY_PULL_REQUEST_CREATE,
+      {
+        orgSlug,
+        repoSlug,
+      }
+    );
+
+    const redirectUrl = [
+      request.namedViewsPathMap[AppRoute.AUTH_LOGIN],
+      `?after_login_goto=${getBackHereLink}`,
+    ];
+
+    console.log("getBackHereLink:", getBackHereLink);
+    console.log("redirectUrl:", redirectUrl);
+
+    reply.redirect(302, redirectUrl.join(""));
     return reply;
   }
 
-  const { from_branch } =
-    request.query as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["querystring"];
-  const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]["params"];
-
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });
   const usersService = makeUsersService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestDeleteAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestDeleteAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestDetailsView.ts
@@ -3,7 +3,7 @@ import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility, User } from "@prisma/client";
 // app
 import type { RepositoryFileDiff } from "../../types";
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -19,7 +19,7 @@ const getRepositoryPullRequestDetailsView: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug, pullUid } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestUpdateAction.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -18,7 +18,7 @@ const getRepositoryPullRequestUpdateAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/getRepositoryPullRequestsView.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -15,7 +15,7 @@ import RepositoryPullRequestsView, {
 
 const getRepositoryPullRequestsView: ReqHandler = async (request, reply) => {
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
+    request.params as AppRouteParams[AppRoute.REPOSITORY_PULL_REQUESTS]["params"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/repositoryPullRequests/postRepositoryPullRequestCreateAction.ts
@@ -2,7 +2,7 @@
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
 import type { RepositoryFileDiff } from "../../types";
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { buildRouteLink } from "../../utils/shared";
 // app islands
 import {

...
@@ -34,7 +34,7 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
   }
 
   const { orgSlug, repoSlug } =
-    request.params as AppRoutesParams[CurrentRoute]["params"];
+    request.params as AppRouteParams[CurrentRoute]["params"];
   const {
     description,
     summary,

...
@@ -46,7 +46,7 @@ const postRepositoryPullRequestCreateAction: ReqHandler = async (
     target_parent_org_slug: targetParentOrgSlug,
     target_repository_dest_branch: targetRepoDestBranch,
     target_repository_slug: targetRepoSlug,
-  } = request.body as AppRoutesParams[CurrentRoute]["body"];
+  } = request.body as AppRouteParams[CurrentRoute]["body"];
 
   const orgService = makeOrganizationService({ request });
   const repoService = makeRepositoryService({ request });

app/controllers/repositoryPullRequests/postRepositoryPullRequestMergeAction.ts
@@ -3,7 +3,7 @@ import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { ResourceVisibility } from "@prisma/client";
 // app
 import type { RepositoryFileDiff } from "../../types";
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 // app services
 import { makeOrganizationService } from "../../services/organization";
 import { makePullRequestService } from "../../services/pullRequest";

...
@@ -21,9 +21,9 @@ const postRepositoryPullRequestMergeAction: ReqHandler = async (
   reply
 ) => {
   const { orgSlug, repoSlug, pullUid } =
-    request.params as AppRoutesParams[CurrentRoute]["params"];
+    request.params as AppRouteParams[CurrentRoute]["params"];
   // const { merge_message, merge_summary } =
-  //   request.body as AppRoutesParams[CurrentRoute]["body"];
+  //   request.body as AppRouteParams[CurrentRoute]["body"];
 
   const orgService = makeOrganizationService({ request });
   const prService = makePullRequestService({ request });

app/controllers/syntaxHighlight/highlightCodeAction.ts
@@ -5,7 +5,7 @@ import Prism from "prismjs";
 import { parse as parseHtmlToJson, TextNode, RealNode } from "himalaya";
 // app
 import type { AppThemeScheme } from "../../types";
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { escapeHtmlCode } from "../../utils/shared/escapeHtmlCode";
 
 Prism.languages.prisma = Prism.languages.extend("javascript", {

...
@@ -85,13 +85,13 @@ const getNodesRecursive = (
 
 const highlightCodeAction: ReqHandler = async (request, reply) => {
   const { outputFormat = "html" } =
-    request.params as AppRoutesParams[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]["params"];
+    request.params as AppRouteParams[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]["params"];
 
   const {
     code,
     language,
     theme_scheme: themeScheme,
-  } = request.body as AppRoutesParams[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]["body"];
+  } = request.body as AppRouteParams[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]["body"];
 
   if (request.validationError != null) {
     if (outputFormat === "html") {

app/controllers/theme/setThemeSchemeAction.ts
@@ -1,12 +1,12 @@
 // 1st-party
 import { ReqHandler } from "@ethicdevs/react-monolith";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 
 const setThemeSchemeAction: ReqHandler = async (request, reply) => {
   const { referer } = request.headers;
   const { themeScheme: desiredScheme } =
-    request.params as AppRoutesParams[AppRoute.THEME_SET_SCHEME_ACTION]["params"];
+    request.params as AppRouteParams[AppRoute.THEME_SET_SCHEME_ACTION]["params"];
 
   reply.setCookie("theme_scheme", desiredScheme === "light" ? "light" : "dark");
   return reply.redirect(302, referer || "/");

app/controllers/user/getUserDetailsView.ts
@@ -2,7 +2,7 @@
 import type { ReqHandler } from "@ethicdevs/react-monolith";
 import { User } from "@prisma/client";
 // app
-import { AppRoute, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRouteParams } from "../../routes.defs";
 import { makeUsersService } from "../../services/user";
 // app views
 import UserDetailsView, {

...
@@ -11,7 +11,7 @@ import UserDetailsView, {
 
 const getUserDetailsView: ReqHandler = async (request, reply) => {
   const { username } =
-    request.params as AppRoutesParams[AppRoute.USER_DETAILS]["params"];
+    request.params as AppRouteParams[AppRoute.USER_DETAILS]["params"];
 
   const { curr_user_uid } = request.session.data;
 

@@ -94,7 +94,7 @@ export const AppRoutePaths: Record<AppRoute, string> = {
   [AppRoute.USER_DETAILS]: "/@:username",
 };
 
-export interface AppRoutesParams extends IRouteParams {
+export interface AppRouteParams extends IRouteParams {
   [AppRoute.HOME]: undefined;
   [AppRoute.THEME_SET_SCHEME_ACTION]: {
     params: {

...
@@ -109,7 +109,11 @@ export interface AppRoutesParams extends IRouteParams {
       password: string;
     };
   };
-  [AppRoute.AUTH_LOGIN]: undefined;
+  [AppRoute.AUTH_LOGIN]: {
+    querystring: {
+      after_login_goto?: string;
+    };
+  };
   [AppRoute.AUTH_LOGIN_ACTION]: {
     body: {
       email_address: string;

...
@@ -326,7 +330,7 @@ export interface AppRoutesParams extends IRouteParams {
   };
 }
 
-export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
+export const AppRouteSchemas: Record<AppRoute, undefined | FastifySchema> = {
   [AppRoute.HOME]: undefined,
   [AppRoute.THEME_SET_SCHEME_ACTION]: {
     params: {

...
@@ -354,7 +358,16 @@ export const AppRoutesSchemas: Record<AppRoute, undefined | FastifySchema> = {
       },
     },
   },
-  [AppRoute.AUTH_LOGIN]: undefined,
+  [AppRoute.AUTH_LOGIN]: {
+    querystring: {
+      type: "object",
+      required: [],
+      additionalProperties: false,
+      properties: {
+        after_login_goto: { type: "string" },
+      },
+    },
+  },
   [AppRoute.AUTH_LOGIN_ACTION]: {
     body: {
       type: "object",

@@ -3,7 +3,7 @@ import { AppRouter, AppRouterGroup, Router } from "@ethicdevs/react-monolith";
 // 3rd-party
 import React from "react";
 // app
-import { AppRoute, AppRoutePaths, AppRoutesSchemas } from "./routes.defs";
+import { AppRoute, AppRoutePaths, AppRouteSchemas } from "./routes.defs";
 import { authenticatedOrLogin, guestOrRedirect } from "./utils/server";
 // app controllers
 import {

...
@@ -31,7 +31,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.THEME_SET_SCHEME_ACTION}
           method={"GET"}
           path={AppRoutePaths[AppRoute.THEME_SET_SCHEME_ACTION]}
-          schema={AppRoutesSchemas[AppRoute.THEME_SET_SCHEME_ACTION]}
+          schema={AppRouteSchemas[AppRoute.THEME_SET_SCHEME_ACTION]}
           handler={ThemeController.setThemeSchemeAction}
         />
         {/* --- */}

...
@@ -55,7 +55,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.AUTH_REGISTER_ACTION]}
           preHandler={guestOrDashboardRedirect}
-          schema={AppRoutesSchemas[AppRoute.AUTH_REGISTER_ACTION]}
+          schema={AppRouteSchemas[AppRoute.AUTH_REGISTER_ACTION]}
           handler={AuthController.postRegisterAction}
         />
         {/* --- */}

...
@@ -64,6 +64,7 @@ const RootAppRouter: AppRouter = () => {
           method={"GET"}
           path={AppRoutePaths[AppRoute.AUTH_LOGIN]}
           preHandler={guestOrDashboardRedirect}
+          schema={AppRouteSchemas[AppRoute.AUTH_LOGIN]}
           handler={AuthController.getLoginView}
         />
         <Router.Route

...
@@ -71,7 +72,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.AUTH_LOGIN_ACTION]}
           preHandler={guestOrDashboardRedirect}
-          schema={AppRoutesSchemas[AppRoute.AUTH_LOGIN_ACTION]}
+          schema={AppRouteSchemas[AppRoute.AUTH_LOGIN_ACTION]}
           handler={AuthController.postLoginAction}
         />
         <Router.Route

...
@@ -79,7 +80,7 @@ const RootAppRouter: AppRouter = () => {
           method={"GET"}
           path={AppRoutePaths[AppRoute.AUTH_LOGOUT_ACTION]}
           preHandler={loggedOrLoginRedirect}
-          schema={AppRoutesSchemas[AppRoute.AUTH_LOGOUT_ACTION]}
+          schema={AppRouteSchemas[AppRoute.AUTH_LOGOUT_ACTION]}
           handler={AuthController.getLogoutAction}
         />
         {/* --- */}

...
@@ -101,7 +102,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.ORGANIZATION_DETAILS}
           method={"GET"}
           path={AppRoutePaths[AppRoute.ORGANIZATION_DETAILS]}
-          schema={AppRoutesSchemas[AppRoute.ORGANIZATION_DETAILS]}
+          schema={AppRouteSchemas[AppRoute.ORGANIZATION_DETAILS]}
           handler={OrganizationController.getOrganizationDetailsView}
         />
         {/* --- */}

...
@@ -109,28 +110,28 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_BROWSER}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_BROWSER]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_BROWSER]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_BROWSER]}
           handler={RepositoryController.getRepositoryBrowserView}
         />
         <Router.Route
           name={AppRoute.REPOSITORY_BROWSER_WITH_PATH}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_BROWSER_WITH_PATH]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_BROWSER]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_BROWSER]}
           handler={RepositoryController.getRepositoryBrowserView}
         />
         <Router.Route
           name={AppRoute.REPOSITORY_COMMITS_LOG}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_COMMITS_LOG]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_COMMITS_LOG]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_COMMITS_LOG]}
           handler={RepositoryController.getRepositoryCommitsLogView}
         />
         <Router.Route
           name={AppRoute.REPOSITORY_COMPARE}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_COMPARE]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_COMPARE]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_COMPARE]}
           handler={RepositoryController.getRepositoryCompareView}
         />
         <Router.Route

...
@@ -145,21 +146,21 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_CREATE_ACTION]}
           preHandler={loggedOrLoginRedirect}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_CREATE_ACTION]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_CREATE_ACTION]}
           handler={RepositoryController.postRepositoryCreateAction}
         />
         <Router.Route
           name={AppRoute.REPOSITORY_DETAILS}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_DETAILS]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_DETAILS]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_DETAILS]}
           handler={RepositoryController.getRepositoryDetailsView}
         />
         <Router.Route
           name={AppRoute.REPOSITORY_DETAILS_WITH_TRAILING_SLASH}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_DETAILS_WITH_TRAILING_SLASH]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_DETAILS]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_DETAILS]}
           handler={RepositoryController.getRepositoryDetailsView}
         />
         <Router.Route

...
@@ -187,7 +188,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_CLOSE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CLOSE_ACTION]
+            AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CLOSE_ACTION]
           }
           preHandler={loggedOrLoginRedirect}
           handler={

...
@@ -203,7 +204,7 @@ const RootAppRouter: AppRouter = () => {
             ]
           }
           schema={
-            AppRoutesSchemas[
+            AppRouteSchemas[
               AppRoute.REPOSITORY_PULL_REQUEST_COMMENT_CREATE_ACTION
             ]
           }

...
@@ -221,7 +222,7 @@ const RootAppRouter: AppRouter = () => {
             ]
           }
           schema={
-            AppRoutesSchemas[
+            AppRouteSchemas[
               AppRoute.REPOSITORY_PULL_REQUEST_COMMENT_DELETE_ACTION
             ]
           }

...
@@ -239,7 +240,7 @@ const RootAppRouter: AppRouter = () => {
             ]
           }
           schema={
-            AppRoutesSchemas[
+            AppRouteSchemas[
               AppRoute.REPOSITORY_PULL_REQUEST_COMMENT_UPDATE_ACTION
             ]
           }

...
@@ -252,7 +253,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_PULL_REQUEST_CREATE}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CREATE]}
           preHandler={loggedOrLoginRedirect}
           handler={
             RepositoryPullRequestsController.getRepositoryPullRequestCreateView

...
@@ -263,7 +264,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]
+            AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_CREATE_ACTION]
           }
           preHandler={loggedOrLoginRedirect}
           handler={

...
@@ -275,7 +276,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_DELETE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_DELETE_ACTION]
+            AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_DELETE_ACTION]
           }
           preHandler={loggedOrLoginRedirect}
           handler={

...
@@ -286,7 +287,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_PULL_REQUEST_DETAILS}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_DETAILS]}
           preHandler={loggedOrLoginRedirect}
           handler={
             RepositoryPullRequestsController.getRepositoryPullRequestDetailsView

...
@@ -297,7 +298,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_MERGE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_MERGE_ACTION]
+            AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_MERGE_ACTION]
           }
           preHandler={loggedOrLoginRedirect}
           handler={

...
@@ -309,7 +310,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUEST_UPDATE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUEST_UPDATE_ACTION]
+            AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUEST_UPDATE_ACTION]
           }
           preHandler={loggedOrLoginRedirect}
           handler={

...
@@ -320,7 +321,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_PULL_REQUESTS}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_PULL_REQUESTS]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_PULL_REQUESTS]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_PULL_REQUESTS]}
           handler={
             RepositoryPullRequestsController.getRepositoryPullRequestsView
           }

...
@@ -329,7 +330,7 @@ const RootAppRouter: AppRouter = () => {
           name={AppRoute.REPOSITORY_SHOW_OBJECT}
           method={"GET"}
           path={AppRoutePaths[AppRoute.REPOSITORY_SHOW_OBJECT]}
-          schema={AppRoutesSchemas[AppRoute.REPOSITORY_SHOW_OBJECT]}
+          schema={AppRouteSchemas[AppRoute.REPOSITORY_SHOW_OBJECT]}
           handler={RepositoryController.getRepositoryShowObjectView}
         />
         {/* --- */}

...
@@ -338,7 +339,7 @@ const RootAppRouter: AppRouter = () => {
           method={"POST"}
           path={AppRoutePaths[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]}
           schema={
-            AppRoutesSchemas[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]
+            AppRouteSchemas[AppRoute.SYNTAX_HIGHLIGHT_HIGHLIGHT_CODE_ACTION]
           }
           handler={SyntaxHighlightController.highlightCodeAction}
         />

@@ -293,6 +293,7 @@ async function main(): Promise<AppServer> {
         initialSession: {
           sessionId: null,
           authenticated: false,
+          auth_redirect_to: null,
           curr_user_avatar_uri: null,
           curr_user_uid: null,
           curr_user_username: null,

@@ -14,6 +14,7 @@ export type WithThemeSchemeProp = {
 export interface AppSessionData extends Prisma.JsonObject {
   sessionId: null | string;
   authenticated: boolean;
+  auth_redirect_to: null | string;
   flash_message: null | string;
   flash_message_shown_once: boolean;
   curr_user_avatar_uri: null | string;

app/utils/server/authenticatedOrLogin.ts
@@ -5,6 +5,27 @@ import { AppRoute } from "../../routes.defs";
 export const authenticatedOrLogin =
   (): preHandlerHookHandler => async (request, reply) => {
     if (request.session.data.authenticated === false) {
-      reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+      const parts = request.routerPath.split("/");
+      const params = request.params as Record<string, unknown>;
+      const redirectGotoUri = parts.map((part) => {
+        if (part.trim() === "") return part;
+        if (part.startsWith(":")) {
+          const paramKey = part.substring(1);
+          const param = params[paramKey];
+          if (param != null) return param;
+          return part;
+        }
+        return part;
+      });
+
+      const redirectAuthUri = [
+        request.namedViewsPathMap[AppRoute.AUTH_LOGIN],
+        `?after_login_goto=${redirectGotoUri.join("/")}`,
+      ].join("");
+
+      console.log("redirectTo:", redirectAuthUri);
+
+      // reply.redirect(302, request.namedViewsPathMap[AppRoute.AUTH_LOGIN]);
+      reply.redirect(302, redirectAuthUri);
     }
   };

app/utils/shared/buildRouteLink.ts
@@ -1,4 +1,4 @@
-import { AppRoute, AppRoutePaths, AppRoutesParams } from "../../routes.defs";
+import { AppRoute, AppRoutePaths, AppRouteParams } from "../../routes.defs";
 
 interface BuildLinkOptions {
   // @default true

...
@@ -7,8 +7,8 @@ interface BuildLinkOptions {
 
 export default function buildRouteLink<P extends AppRoute>(
   route: P,
-  routeParams: "params" extends keyof AppRoutesParams[P]
-    ? AppRoutesParams[P]["params"]
+  routeParams: "params" extends keyof AppRouteParams[P]
+    ? AppRouteParams[P]["params"]
     : {} | null,
   options?: BuildLinkOptions
 ): typeof AppRoutePaths[P] {

...
@@ -18,8 +18,8 @@ export default function buildRouteLink<P extends AppRoute>(
 
 export function buildPathLink<P extends AppRoute>(
   path: string,
-  routeParams: "params" extends keyof AppRoutesParams[P]
-    ? AppRoutesParams[P]["params"]
+  routeParams: "params" extends keyof AppRouteParams[P]
+    ? AppRouteParams[P]["params"]
     : {} | null,
   options?: BuildLinkOptions
 ): string {