Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
.turbo
.next
*.tsbuildinfo

# Logs
logs
Expand Down
35 changes: 0 additions & 35 deletions packages/cli/src/utils/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -469,40 +469,6 @@ function validateAndInstallMissingDependencies(basePath: string) {
})
}

// TODO: Read the value from an environment variable
const ENABLE_REDIRECTS_MIDDLEWARE = false

// Enable redirects middleware by renaming the file from middleware__DISABLED.ts to middleware.tsß
function enableRedirectsMiddleware(basePath: string) {
if (!ENABLE_REDIRECTS_MIDDLEWARE) {
return
}

try {
const { tmpDir } = withBasePath(basePath)

const disabledMiddlewarePath = path.join(
tmpDir,
'src',
'middleware__DISABLED.ts'
)

/* Rename the file to enable middleware functionality and then remove the disabled middleware file */
if (existsSync(disabledMiddlewarePath)) {
const enabledMiddlewarePath = path.join(tmpDir, 'src', 'middleware.ts')
copyFileSync(disabledMiddlewarePath, enabledMiddlewarePath)
removeSync(disabledMiddlewarePath)

logger.log(
`${chalk.green('success')} Redirects middleware has been enabled`
)
}
} catch (error) {
logger.error(error)
throw error
}
}

function enableSearchSSR(basePath: string) {
const storeConfigPath = getCurrentUserStoreConfigFile(basePath)

Expand Down Expand Up @@ -551,7 +517,6 @@ export async function generate(options: GenerateOptions) {
copyUserStarterToCustomizations(basePath),
copyTheme(basePath),
createCmsWebhookUrlsJsonFile(basePath),
enableRedirectsMiddleware(basePath),

installPlugins(basePath),
])
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
"idb-keyval": "^5.1.3",
"include-media": "^1.4.10",
"isomorphic-unfetch": "^3.1.0",
"jose": "^6.1.3",
"lexical": "^0.34.0",
"next": "^13.5.9",
"next-seo": "^6.6.0",
Expand Down
83 changes: 83 additions & 0 deletions packages/core/src/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Middleware is always active. It runs:
* 1. Password protection (AuthenticationService) — when applicable (default/custom domains per env).
* 2. Redirects — only when ENABLE_REDIRECTS_MIDDLEWARE is set at runtime.
*/

import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import storeConfig from 'discovery.config'

import { AuthenticationService } from './server/authentication-service'

type Redirect = {
from: string
to: string
type: 'permanent' | 'temporary'
}
interface RedirectsClient {
get(from: string): Promise<Redirect | null>
}

class DynamoRedirectsClient implements RedirectsClient {
async get(_from: string): Promise<Redirect | null> {
// TODO: Implement DynamoDB client. Ensure that the cluster has access to DynamoDB first.
return null
}
}

const redirectsClient = new DynamoRedirectsClient()

export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname

try {
const authService = new AuthenticationService()
const authResult = await authService.authenticateRequest(request)

if (authResult.response.status !== 200) {
return authResult.response
}

if (process.env.ENABLE_REDIRECTS_MIDDLEWARE === 'true') {
const redirect = await redirectsClient.get(path)

if (redirect) {
const redirectUrl = new URL(redirect.to, storeConfig.storeUrl)
const redirectStatusCode = redirect.type === 'permanent' ? 301 : 302
const response = NextResponse.redirect(redirectUrl, redirectStatusCode)

for (const cookie of authResult.response.cookies.getAll()) {
response.cookies.set(cookie)
}

response.headers.set(
'Cache-Control',
'public, max-age=300, stale-while-revalidate=31536000'
)

return response
}
}

return authResult.response
} catch {
return NextResponse.next()
}
Comment on lines +34 to +66
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don't fail open when auth throws.

The outer catch turns any unexpected auth error into NextResponse.next(), which exposes protected previews instead of keeping the request behind the gate. Keep the auth path outside that fail-open branch, and only soften redirect lookup failures if needed.

🩹 Proposed fix
 export async function middleware(request: NextRequest) {
   const path = request.nextUrl.pathname
+  const authService = new AuthenticationService()
+  const authResult = await authService.authenticateRequest(request)
 
-  try {
-    const authService = new AuthenticationService()
-    const authResult = await authService.authenticateRequest(request)
-
-    if (authResult.response.status !== 200) {
-      return authResult.response
-    }
+  if (authResult.response.status !== 200) {
+    return authResult.response
+  }
 
-    if (process.env.ENABLE_REDIRECTS_MIDDLEWARE === 'true') {
+  if (process.env.ENABLE_REDIRECTS_MIDDLEWARE === 'true') {
+    try {
       const redirect = await redirectsClient.get(path)
 
       if (redirect) {
         const redirectUrl = new URL(redirect.to, storeConfig.storeUrl)
         const redirectStatusCode = redirect.type === 'permanent' ? 301 : 302
@@
 
         return response
       }
+    } catch {
+      return authResult.response
     }
-
-    return authResult.response
-  } catch {
-    return NextResponse.next()
   }
+
+  return authResult.response
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const authService = new AuthenticationService()
const authResult = await authService.authenticateRequest(request)
if (authResult.response.status !== 200) {
return authResult.response
}
if (process.env.ENABLE_REDIRECTS_MIDDLEWARE === 'true') {
const redirect = await redirectsClient.get(path)
if (redirect) {
const redirectUrl = new URL(redirect.to, storeConfig.storeUrl)
const redirectStatusCode = redirect.type === 'permanent' ? 301 : 302
const response = NextResponse.redirect(redirectUrl, redirectStatusCode)
response.headers.set(
'Cache-Control',
'public, max-age=300, stale-while-revalidate=31536000'
)
return response
}
}
return authResult.response
} catch {
return NextResponse.next()
}
const authService = new AuthenticationService()
const authResult = await authService.authenticateRequest(request)
if (authResult.response.status !== 200) {
return authResult.response
}
if (process.env.ENABLE_REDIRECTS_MIDDLEWARE === 'true') {
try {
const redirect = await redirectsClient.get(path)
if (redirect) {
const redirectUrl = new URL(redirect.to, storeConfig.storeUrl)
const redirectStatusCode = redirect.type === 'permanent' ? 301 : 302
const response = NextResponse.redirect(redirectUrl, redirectStatusCode)
response.headers.set(
'Cache-Control',
'public, max-age=300, stale-while-revalidate=31536000'
)
return response
}
} catch {
return authResult.response
}
}
return authResult.response
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/middleware.ts` around lines 34 - 62, Move the
authentication call (new AuthenticationService().authenticateRequest(request))
out of the outer try/catch so auth errors are not swallowed; if
authenticateRequest throws, return a failing auth response (do not return
NextResponse.next()) or let the error propagate to produce a non-public
response. Wrap only the redirect lookup (redirectsClient.get(path) and
subsequent URL/NextResponse.redirect logic) in a try/catch and on redirect
lookup failure fall back to returning authResult.response; keep references to
AuthenticationService, authenticateRequest, redirectsClient.get,
NextResponse.redirect, and NextResponse.next() to locate the code to change.

}

export const config = {
matcher: [
/*
* Match all paths including `/`
* Exclude:
* - api (e.g. api/fs/auth/login)
* - _next/static, _next/image
* - favicon.ico
* - fs-auth-login (login page)
* - ~partytown (partytown scripts)
*/
'/',
'/((?!api|_next/static|_next/image|favicon.ico|fs-auth-login|~partytown).*)',
],
}
65 changes: 0 additions & 65 deletions packages/core/src/middleware__DISABLED.ts

This file was deleted.

98 changes: 98 additions & 0 deletions packages/core/src/pages/api/fs/auth/login.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import type { NextApiHandler, NextApiRequest, NextApiResponse } from 'next'

import storeConfig from 'discovery.config'

import { isSecureAuthCookieForPagesApi } from '../../../../server/password-protection/auth-cookie'
import {
sessionUrl,
passwordProtectionTimeouts,
} from '../../../../server/password-protection/webops-api'

const COOKIE_NAME = '__fs_auth_token'
const TOKEN_TTL_SECONDS = 10 * 60

const isSafeReturnToPath = (value: string): boolean => {
return value.startsWith('/') && !value.startsWith('//')
}

const handler: NextApiHandler = async (
request: NextApiRequest,
response: NextApiResponse
) => {
if (request.method !== 'POST') {
response.status(405).end()
return
}

const storeId = storeConfig.api.storeId

try {
const { password } = request.body ?? {}

if (!password || typeof password !== 'string') {
response.status(400).json({
success: false,
error: 'Password is required',
})
return
}

const webopsResponse = await fetch(sessionUrl(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ storeId, password }),
signal: AbortSignal.timeout(passwordProtectionTimeouts.defaultMs),
})

if (!webopsResponse.ok) {
if (webopsResponse.status === 401 || webopsResponse.status === 403) {
response.status(401).json({
success: false,
error: 'Invalid password',
})
return
}

response.status(503).json({
success: false,
error: 'Service temporarily unavailable',
})
return
}
Comment thread
vlaux marked this conversation as resolved.

const data = await webopsResponse.json()

if (data.valid && data.token) {
const returnTo =
typeof request.query.returnTo === 'string'
? request.query.returnTo
: '/'
const sanitizedReturnTo = isSafeReturnToPath(returnTo) ? returnTo : '/'

const securePart = isSecureAuthCookieForPagesApi(request)
? '; Secure'
: ''

response.setHeader('Set-Cookie', [
`${COOKIE_NAME}=${data.token}; HttpOnly${securePart}; SameSite=Lax; Path=/; Max-Age=${TOKEN_TTL_SECONDS}`,
])

response.status(200).json({
success: true,
redirectUrl: sanitizedReturnTo,
})
} else {
response.status(401).json({
success: false,
error: 'Invalid password',
})
}
} catch {
response.status(503).json({
success: false,
error: 'Service temporarily unavailable',
})
}
}

export default handler
41 changes: 41 additions & 0 deletions packages/core/src/pages/fs-auth-login/fs-auth-login.module.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
@layer components {
.fsAuthLogin {
@import "@faststore/ui/src/components/atoms/Button/styles.scss";
@import "@faststore/ui/src/components/atoms/Input/styles.scss";
@import "@faststore/ui/src/components/atoms/Loader/styles.scss";
@import "@faststore/ui/src/components/molecules/InputField/styles.scss";
}
}

.page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: var(--fs-spacing-3);
}

.title {
margin: 0 0 var(--fs-spacing-6) 0;
font-size: var(--fs-text-size-title-section);
font-weight: var(--fs-text-weight-bold);
}

.subtitle {
margin: 0 0 var(--fs-spacing-3) 0;
font-size: var(--fs-text-size-body);
}

.form {
display: flex;
flex-direction: column;
gap: var(--fs-spacing-2);
align-items: stretch;
width: 100%;
max-width: 20rem;

button {
align-self: center;
}
}
Loading
Loading