-
Notifications
You must be signed in to change notification settings - Fork 78
feat: password protected stores #3276
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 8 commits
361a415
f247d80
b087d22
7d9eef2
cfe7d95
cdd81e1
d1c12a3
d4400ee
f79a573
77fe5fd
1f0f960
3cfaa4e
bc89347
5e7f321
9c7f6de
5fad874
4850045
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,6 @@ | ||
| .turbo | ||
| .next | ||
| *.tsbuildinfo | ||
|
|
||
| # Logs | ||
| logs | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,79 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /* | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| * 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) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't fail open when auth throws. The outer 🩹 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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).*)', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| 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 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) { | ||
| response.status(401).json({ | ||
| success: false, | ||
| error: 'Invalid password', | ||
| }) | ||
| return | ||
| } | ||
|
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 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: returnTo, | ||
|
vlaux marked this conversation as resolved.
Outdated
|
||
| }) | ||
| } else { | ||
| response.status(401).json({ | ||
| success: false, | ||
| error: 'Invalid password', | ||
| }) | ||
| } | ||
| } catch { | ||
| response.status(503).json({ | ||
| success: false, | ||
| error: 'Service temporarily unavailable', | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| export default handler | ||
| 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; | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.