mirror of
https://github.com/stack-auth/stack.git
synced 2026-06-13 21:01:21 +08:00
Middleware support (#29)
This commit is contained in:
parent
a108bb11a4
commit
74f3fe2e49
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --port 8106",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start --port 8106",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -8,7 +8,7 @@
|
||||
"clean": "rimraf .next && rimraf node_modules",
|
||||
"dev": "next dev --port 8103",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start --port 8103",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
4
apps/middleware/.env
Normal file
4
apps/middleware/.env
Normal file
@ -0,0 +1,4 @@
|
||||
NEXT_PUBLIC_STACK_URL=# enter your stack endpoint here, e.g. http://localhost:8101
|
||||
NEXT_PUBLIC_STACK_PROJECT_ID=# enter your stack project id here
|
||||
NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# enter your stack publishable client key here
|
||||
STACK_SECRET_SERVER_KEY=# enter your stack secret server key here
|
||||
7
apps/middleware/.eslintrc.js
Normal file
7
apps/middleware/.eslintrc.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
"extends": [
|
||||
"../../eslint-configs/defaults.js",
|
||||
"../../eslint-configs/next.js",
|
||||
],
|
||||
"ignorePatterns": ['/*', '!/src']
|
||||
};
|
||||
36
apps/middleware/.gitignore
vendored
Normal file
36
apps/middleware/.gitignore
vendored
Normal file
@ -0,0 +1,36 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
.yarn/install-state.gz
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env*.local
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
7
apps/middleware/LICENSE
Normal file
7
apps/middleware/LICENSE
Normal file
@ -0,0 +1,7 @@
|
||||
Copyright 2024 Stackframe
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
36
apps/middleware/README.md
Normal file
36
apps/middleware/README.md
Normal file
@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
||||
8
apps/middleware/next.config.mjs
Normal file
8
apps/middleware/next.config.mjs
Normal file
@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
experimental: {
|
||||
ppr: true,
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
25
apps/middleware/package.json
Normal file
25
apps/middleware/package.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "middleware-demo",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --port 8107",
|
||||
"build": "next build",
|
||||
"start": "next start --port 8107",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "^14.3.0-canary.26",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"@stackframe/stack": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^18",
|
||||
"@types/react-dom": "^18",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.3",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
10
apps/middleware/src/app/handler/[...stack]/page.tsx
Normal file
10
apps/middleware/src/app/handler/[...stack]/page.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import { StackHandler } from "@stackframe/stack";
|
||||
import { stackServerApp } from "../../../stack";
|
||||
|
||||
export default function Handler(props: any) {
|
||||
return (
|
||||
<div style={{ backgroundColor: "white", borderRadius: 4 }}>
|
||||
<StackHandler app={stackServerApp} {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
29
apps/middleware/src/app/layout.tsx
Normal file
29
apps/middleware/src/app/layout.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import type { Metadata } from "next";
|
||||
import { StackProvider, StackTheme } from "@stackframe/stack";
|
||||
import { stackServerApp } from "../stack";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Stack Auth Middleware Demo",
|
||||
description: "A demo of Stack's middleware capabilities.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode,
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={inter.className}>
|
||||
<StackProvider app={stackServerApp}>
|
||||
<StackTheme>
|
||||
{children}
|
||||
</StackTheme>
|
||||
</StackProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
3
apps/middleware/src/app/loading.tsx
Normal file
3
apps/middleware/src/app/loading.tsx
Normal file
@ -0,0 +1,3 @@
|
||||
export default function Loading() {
|
||||
return <></>;
|
||||
}
|
||||
12
apps/middleware/src/app/page.tsx
Normal file
12
apps/middleware/src/app/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { stackServerApp } from "@/stack";
|
||||
import Link from "next/link";
|
||||
|
||||
export default async function Home() {
|
||||
return (
|
||||
<main>
|
||||
Page 1 (not protected)<br />
|
||||
Current login status: {await stackServerApp.getUser() ? 'Logged in' : 'Not logged in'}<br />
|
||||
<Link href="/protected">Go to Page 2</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
12
apps/middleware/src/app/protected/page.tsx
Normal file
12
apps/middleware/src/app/protected/page.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import { stackServerApp } from "@/stack";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<main>
|
||||
Page 2 (protected by middleware)<br />
|
||||
<Link href="/handler/signout">Sign out</Link><br />
|
||||
<Link href="/">Go to Page 1</Link>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
22
apps/middleware/src/middleware.tsx
Normal file
22
apps/middleware/src/middleware.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import type { NextRequest } from 'next/server';
|
||||
import { stackServerApp } from './stack';
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
console.log('Running middleware on URL: ', request.url);
|
||||
|
||||
// fetch the user object, and redirect if not logged in
|
||||
const user = await stackServerApp.getUser();
|
||||
if (!user) {
|
||||
console.log('User in middleware is not logged in. Redirecting to /handler/signin');
|
||||
return NextResponse.redirect(new URL('/handler/signin', request.url));
|
||||
}
|
||||
|
||||
console.log('User in middleware is logged in. ID: ', user.id);
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: '/protected/:path*',
|
||||
};
|
||||
8
apps/middleware/src/stack.tsx
Normal file
8
apps/middleware/src/stack.tsx
Normal file
@ -0,0 +1,8 @@
|
||||
import "server-only";
|
||||
|
||||
import { StackServerApp } from "@stackframe/stack";
|
||||
|
||||
export const stackServerApp = new StackServerApp({
|
||||
tokenStore: "nextjs-cookie",
|
||||
});
|
||||
(stackServerApp as any).__DEMO_ENABLE_SLIGHT_FETCH_DELAY = true;
|
||||
40
apps/middleware/tsconfig.json
Normal file
40
apps/middleware/tsconfig.json
Normal file
@ -0,0 +1,40 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
},
|
||||
"target": "ES2017"
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
@ -5,7 +5,7 @@
|
||||
"scripts": {
|
||||
"dev": "next dev --port 8105",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"start": "next start --port 8105",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
"docusaurus": "dotenv -c -- docusaurus",
|
||||
"clean": "pnpm docusaurus clear && rimraf node_modules",
|
||||
"dev": "pnpm docusaurus start --port 8104 --no-open",
|
||||
"start": "pnpm docusaurus start",
|
||||
"start": "pnpm docusaurus start --port 8104 --no-open",
|
||||
"build": "pnpm docusaurus build",
|
||||
"swizzle": "pnpm docusaurus swizzle",
|
||||
"deploy": "pnpm docusaurus deploy",
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"dev:app": "turbo run dev --continue --filter=@stackframe/dev-app...",
|
||||
"dev:server": "turbo run dev --continue --filter=@stackframe/stack-server...",
|
||||
"dev:email": "turbo run email --continue --filter=@stackframe/stack-server...",
|
||||
"start": "turbo run start --parallel --continue",
|
||||
"start:server": "turbo run start --continue --filter=@stackframe/stack-server...",
|
||||
"docs": "turbo run dev --parallel --continue --filter=stack-docs...",
|
||||
"lint": "turbo run lint --no-cache -- --max-warnings=0",
|
||||
|
||||
@ -1 +1 @@
|
||||
export const cookies = undefined;
|
||||
export { cookies } from 'next/headers';
|
||||
|
||||
@ -5,14 +5,22 @@ import type { NextRequest } from 'next/server';
|
||||
import { deindent } from '@stackframe/stack-shared/dist/utils/strings';
|
||||
|
||||
const corsAllowedRequestHeaders = [
|
||||
// General
|
||||
'authorization',
|
||||
'content-type',
|
||||
'x-stack-project-id',
|
||||
'x-stack-override-error-status',
|
||||
|
||||
// Project auth
|
||||
'x-stack-request-type',
|
||||
'x-stack-publishable-client-key',
|
||||
'x-stack-secret-server-key',
|
||||
'x-stack-super-secret-admin-key',
|
||||
'x-stack-admin-access-token',
|
||||
'x-stack-override-error-status',
|
||||
|
||||
// User auth
|
||||
'x-stack-user-refresh-token',
|
||||
'x-stack-user-access-token',
|
||||
];
|
||||
|
||||
const corsAllowedResponseHeaders = [
|
||||
|
||||
@ -35,7 +35,12 @@ export function registerErrorSink(sink: (location: string, error: unknown) => vo
|
||||
}
|
||||
errorSinks.add(sink);
|
||||
}
|
||||
registerErrorSink((location, ...args) => console.error(`Error in ${location}:`, ...args));
|
||||
registerErrorSink((location, ...args) => {
|
||||
console.error(`Error in ${location}:`, ...args);
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
debugger;
|
||||
}
|
||||
});
|
||||
|
||||
export function captureError(location: string, error: unknown): void {
|
||||
for (const sink of errorSinks) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useRef, useEffect } from "react";
|
||||
import { useRef, useEffect, useState } from "react";
|
||||
import { useStackApp } from "..";
|
||||
import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import MessageCard from "../components/message-card";
|
||||
@ -8,12 +8,23 @@ import MessageCard from "../components/message-card";
|
||||
export default function OAuthCallback () {
|
||||
const app = useStackApp();
|
||||
const called = useRef(false);
|
||||
const [error, setError] = useState<unknown>(null);
|
||||
|
||||
useEffect(() => runAsynchronously(async () => {
|
||||
if (called.current) return;
|
||||
called.current = true;
|
||||
await app.callOAuthCallback();
|
||||
try {
|
||||
await app.callOAuthCallback();
|
||||
} catch (e: any) {
|
||||
setError(e);
|
||||
}
|
||||
}), []);
|
||||
|
||||
return <MessageCard title='Redirecting...' fullPage />;
|
||||
return <MessageCard title='Redirecting...' fullPage>
|
||||
{error ? <div>
|
||||
<p>Something went wrong while processing the OAuth callback:</p>
|
||||
<pre>{JSON.stringify(error, null, 2)}</pre>
|
||||
<p>This is most likely an error in Stack. Please report it.</p>
|
||||
</div> : null}
|
||||
</MessageCard>;
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { StackClientInterface } from "@stackframe/stack-shared";
|
||||
import { saveVerifierAndState, getVerifierAndState } from "./cookie";
|
||||
import { constructRedirectUrl } from "../utils/url";
|
||||
import { TokenStore } from "@stackframe/stack-shared/dist/interface/clientInterface";
|
||||
import { neverResolve } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { neverResolve, wait } from "@stackframe/stack-shared/dist/utils/promises";
|
||||
import { StackAssertionError } from "@stackframe/stack-shared/dist/utils/errors";
|
||||
|
||||
export async function signInWithOAuth(
|
||||
|
||||
@ -2,12 +2,20 @@ import { generateRandomCodeVerifier, generateRandomState, calculatePKCECodeChall
|
||||
import Cookies from "js-cookie";
|
||||
import { cookies as rscCookies } from '@stackframe/stack-sc';
|
||||
|
||||
function isCookieUnavailableError(e: any) {
|
||||
const allowedMessageSnippets = ["was called outside a request scope", "cookies() expects to have requestAsyncStorage"];
|
||||
return typeof e?.message === "string" && allowedMessageSnippets.some(msg => e.message.includes(msg));
|
||||
}
|
||||
|
||||
export function getCookie(name: string): string | null {
|
||||
// TODO the differentiating factor should be RCC vs. RSC, not whether it's a client
|
||||
if (rscCookies) {
|
||||
try {
|
||||
return rscCookies().get(name)?.value ?? null;
|
||||
} else {
|
||||
return Cookies.get(name) ?? null;
|
||||
} catch (e: any) {
|
||||
if (isCookieUnavailableError(e)) {
|
||||
return Cookies.get(name) ?? null;
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,18 +28,31 @@ export function setOrDeleteCookie(name: string, value: string | null) {
|
||||
}
|
||||
|
||||
export function deleteCookie(name: string) {
|
||||
if (rscCookies) {
|
||||
try {
|
||||
rscCookies().delete(name);
|
||||
} else {
|
||||
Cookies.remove(name);
|
||||
} catch (e: any) {
|
||||
if (isCookieUnavailableError(e)) {
|
||||
Cookies.remove(name);
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function setCookie(name: string, value: string) {
|
||||
if (rscCookies) {
|
||||
rscCookies().set(name, value);
|
||||
} else {
|
||||
Cookies.set(name, value, { secure: window.location.protocol === "https:" });
|
||||
export function setCookie(name: string, value: string, options: { maxAge?: number } = {}) {
|
||||
try {
|
||||
rscCookies().set(name, value, {
|
||||
maxAge: options.maxAge,
|
||||
});
|
||||
} catch (e: any) {
|
||||
if (isCookieUnavailableError(e)) {
|
||||
Cookies.set(name, value, {
|
||||
secure: window.location.protocol === "https:",
|
||||
expires: options.maxAge === undefined ? undefined : new Date(Date.now() + (options.maxAge) * 1000),
|
||||
});
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -638,10 +638,12 @@ class _StackClientAppImpl<HasTokenStore extends boolean, ProjectId extends strin
|
||||
this._ensurePersistentTokenStore();
|
||||
const tokenStore = getTokenStore(this._tokenStoreOptions);
|
||||
const result = await callOAuthCallback(this._interface, tokenStore, this.urls.oauthCallback);
|
||||
if (result?.newUser) {
|
||||
window.location.replace(this.urls.afterSignUp);
|
||||
} else {
|
||||
window.location.replace(this.urls.afterSignIn);
|
||||
if (result) {
|
||||
if (result.newUser) {
|
||||
window.location.replace(this.urls.afterSignUp);
|
||||
} else {
|
||||
window.location.replace(this.urls.afterSignIn);
|
||||
}
|
||||
}
|
||||
await neverResolve();
|
||||
}
|
||||
|
||||
@ -177,6 +177,40 @@ importers:
|
||||
specifier: 7.4.1
|
||||
version: 7.4.1
|
||||
|
||||
apps/middleware:
|
||||
dependencies:
|
||||
'@stackframe/stack':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/stack
|
||||
next:
|
||||
specifier: ^14.3.0-canary.26
|
||||
version: 14.3.0-canary.38(@babel/core@7.24.0)(react-dom@18.2.0)(react@18.2.0)
|
||||
react:
|
||||
specifier: ^18
|
||||
version: 18.2.0
|
||||
react-dom:
|
||||
specifier: ^18
|
||||
version: 18.2.0(react@18.2.0)
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: ^20
|
||||
version: 20.11.18
|
||||
'@types/react':
|
||||
specifier: ^18
|
||||
version: 18.2.66
|
||||
'@types/react-dom':
|
||||
specifier: ^18
|
||||
version: 18.2.18
|
||||
eslint:
|
||||
specifier: ^8
|
||||
version: 8.30.0
|
||||
eslint-config-next:
|
||||
specifier: 14.2.3
|
||||
version: 14.2.3(eslint@8.30.0)(typescript@5.3.3)
|
||||
typescript:
|
||||
specifier: ^5
|
||||
version: 5.3.3
|
||||
|
||||
apps/partial-prerendering:
|
||||
dependencies:
|
||||
'@stackframe/stack':
|
||||
|
||||
Loading…
Reference in New Issue
Block a user