1
0
mirror of synced 2026-02-04 03:01:17 -05:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Siddharth Suresh
0196ea3810 test in toolkit app 2023-01-30 19:00:25 +05:30
Siddharth Suresh
28e77ef1c3 hacky fix params export from browser 2023-01-30 13:36:08 +05:30
Siddharth Suresh
072660fb39 working mostly? 2023-01-30 01:27:57 +05:30
93 changed files with 2919 additions and 320 deletions

View File

@@ -1,6 +1,6 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
pnpm manypkg check
pnpm lint
# pnpm manypkg check
# pnpm lint
pnpm pretty-quick --staged

7
apps/next13/.env Normal file
View File

@@ -0,0 +1,7 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="file:./dev.db"

View File

@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

38
apps/next13/.gitignore vendored Normal file
View File

@@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
.vscode

26
apps/next13/README.md Normal file
View File

@@ -0,0 +1,26 @@
# Next.js 13 + Blitz Auth
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) + [`Blitz Auth`](https://blitzjs.com/docs/auth).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can go to the `/signup` page and create a new account.
## Learn More
To learn more about Next.js and Blitz.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.
- [Blitz.js Documentation](https://blitzjs.com/docs/) — learn about Blitz.js.
- [Blitz Auth Documentation](https://blitzjs.com/docs/auth) — learn about Blitz Auth plugin.

View File

@@ -0,0 +1,22 @@
"use client"
import {LoginForm} from "../../../src/auth/components/LoginForm"
import {useRouter} from "next/navigation"
import {useSearchParams} from "next/navigation"
const LoginPage = () => {
const router = useRouter()
const searchParams = useSearchParams()
return (
<LoginForm
onSuccess={(_user) => {
const next = searchParams.get("next")
? decodeURIComponent(searchParams.get("next") as string)
: "/"
return router.push(next)
}}
/>
)
}
export default LoginPage

View File

@@ -0,0 +1,11 @@
"use client"
import {useRouter} from "next/navigation"
import SignupForm from "../../../src/auth/components/SignupForm"
const SignUp = () => {
const router = useRouter()
return <SignupForm onSuccess={() => router.push("/")} />
}
export default SignUp

24
apps/next13/app/error.tsx Normal file
View File

@@ -0,0 +1,24 @@
"use client" // Error components must be Client components
import {useEffect} from "react"
export default function Error({error, reset}: {error: Error; reset: () => void}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error)
}, [error])
return (
<div>
<h2>Something went wrong!</h2>
<button
onClick={
// Attempt to recover by trying to re-render the segment
() => reset()
}
>
Try again
</button>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import "src/styles/globals.css"
import BlitzProvider from "./provider"
import styles from "src/styles/Home.module.css"
export default function RootLayout({children}: {children: React.ReactNode}) {
return (
<html lang="en">
<head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</head>
<body>
<BlitzProvider>
<div className={styles.globe} />
<div className={styles.container}>
<div className={styles.toastContainer}>
<p>
<strong>Congrats!</strong> Your app is ready, including user sign-up and log-in.
</p>
</div>
<main className={styles.main}>
<div className={styles.wrapper}>
<div className={styles.header}>
<div className={styles.logo}>
<svg viewBox="0 0 165 66">
<path d="M104.292 56.033C104.292 56.408 104.206 56.6636 104.036 56.8C103.9 56.9363 103.627 57.0045 103.218 57.0045H99.7409C99.4001 57.0045 99.1615 56.9533 99.0251 56.8511C98.8888 56.7147 98.8206 56.4932 98.8206 56.1864L98.9229 19.8324C98.9229 19.3211 99.1444 19.0654 99.5876 19.0654H103.627C103.839 19.0654 104.292 19.0672 104.292 19.0672V19.8324V56.033ZM64.3531 57.0081C64.1145 57.0081 63.927 56.9399 63.7906 56.8035C63.6543 56.6672 63.5861 56.4968 63.5861 56.2922V19.9383C63.5861 19.3588 63.8588 19.069 64.4042 19.069H76.829C81.533 19.069 85.1463 19.9212 87.6687 21.6256C90.1912 23.2958 91.4524 25.7331 91.4524 28.9373C91.4524 30.9484 90.924 32.6528 89.8673 34.0504C88.8106 35.4138 87.1063 36.5217 84.7543 37.3739C84.6179 37.4079 84.5497 37.4932 84.5497 37.6295C84.5497 37.7318 84.6179 37.7999 84.7543 37.834C87.2767 38.5158 89.1686 39.5895 90.4298 41.0553C91.7251 42.521 92.3727 44.4469 92.3727 46.833C92.3727 50.2418 91.0945 52.7983 88.5379 54.5027C85.9814 56.1729 82.2318 57.0081 77.2892 57.0081H64.3531ZM77.5448 35.5843C79.6923 35.5843 81.516 35.1071 83.0158 34.1526C84.5157 33.1982 85.2656 31.6983 85.2656 29.6531C85.2656 27.6079 84.5157 26.0569 83.0158 25.0002C81.5501 23.9435 79.5219 23.4151 76.9313 23.4151H70.5399C70.0286 23.4151 69.7729 23.6367 69.7729 24.0798V34.8684C69.7729 35.3457 69.9604 35.5843 70.3354 35.5843H77.5448ZM77.0335 52.662C82.9647 52.662 85.9303 50.5997 85.9303 46.4751C85.9303 44.3276 85.1633 42.7255 83.6294 41.6688C82.0955 40.6121 80.0673 40.0838 77.5448 40.0838H70.591C70.2843 40.0838 70.0627 40.1349 69.9263 40.2372C69.8241 40.3394 69.7729 40.5099 69.7729 40.7485V51.895C69.7729 52.4063 69.9604 52.662 70.3354 52.662H77.0335ZM142.707 56.8624C142.81 56.9647 142.997 57.0158 143.27 57.0158H163.876C164.387 57.0158 164.643 56.7772 164.643 56.3V53.948V53.3344H163.978H149.866C149.593 53.3344 149.457 53.2492 149.457 53.0788C149.457 52.9765 149.508 52.8572 149.61 52.7208L163.876 33.8536C164.251 33.2741 164.438 32.7628 164.438 32.3197V30.479V29.9144C164.438 29.9144 164.051 29.9165 163.876 29.9165H144.241C143.866 29.9165 143.679 30.121 143.679 30.5301V32.831C143.679 33.1037 143.713 33.2911 143.781 33.3934C143.883 33.4957 144.071 33.5468 144.344 33.5468H157.075C157.382 33.5468 157.535 33.632 157.535 33.8025L157.382 34.1092L143.219 52.9765C142.946 53.3515 142.759 53.6412 142.656 53.8457C142.588 54.0502 142.554 54.3059 142.554 54.6127V56.3C142.554 56.5727 142.605 56.7602 142.707 56.8624ZM116.929 19.0676H111.51V27.7684C114.503 27.7684 116.929 25.3419 116.929 22.3486V19.0676ZM116.926 56.0308C116.926 56.4058 116.841 56.6614 116.67 56.7978C116.534 56.9341 116.278 57.0023 115.903 57.0023H112.427C112.086 57.0023 111.847 56.9512 111.711 56.8489C111.574 56.7126 111.506 56.491 111.506 56.1842V30.6699C111.506 30.3972 111.557 30.2098 111.66 30.1075C111.762 29.9712 111.949 29.903 112.222 29.903H117.028L116.926 56.0308ZM132.183 34.3137C132.183 33.9728 132.336 33.8024 132.643 33.8024H138.779C139.256 33.8024 139.495 33.5979 139.495 33.1888V30.4789V29.9165H138.881H132.745C132.439 29.9165 132.285 29.7631 132.285 29.4563V21.531V20.713L131.621 20.7129H128.093C127.752 20.7129 127.547 20.9515 127.479 21.4288L126.865 29.4563C126.865 29.7631 126.729 29.9165 126.456 29.9165H122.366C121.957 29.9165 121.752 30.1039 121.752 30.4789V33.1888C121.752 33.5979 121.974 33.8024 122.417 33.8024H126.252C126.593 33.8024 126.763 34.0069 126.763 34.416V50.6244C126.763 52.806 127.309 54.4252 128.399 55.4819C129.49 56.5045 131.16 57.0158 133.41 57.0158C135.796 57.0158 137.535 56.9306 138.625 56.7601C139.137 56.6579 139.392 56.3681 139.392 55.8909V53.6923V53.0787H138.779H135.507C134.348 53.0787 133.495 52.806 132.95 52.2606C132.439 51.7152 132.183 50.7267 132.183 49.295V34.3137Z"></path>
<path d="M0.241243 33.2639H10.9742C15.0585 33.2639 18.9054 35.1835 21.3612 38.4471L31.9483 52.5165C32.1484 52.7824 32.1786 53.1393 32.026 53.435L25.9232 65.2592C25.6304 65.8265 24.8455 65.8932 24.4612 65.3835L0.241243 33.2639Z"></path>
<path d="M42.4727 33.2822H31.7398C27.6555 33.2822 23.8086 31.3626 21.3528 28.0991L10.7656 14.0297C10.5656 13.7638 10.5354 13.4068 10.688 13.1111L16.7908 1.28696C17.0836 0.719654 17.8684 0.652924 18.2528 1.16266L42.4727 33.2822Z"></path>
</svg>
</div>
<div className={styles.buttonContainer}></div>
</div>
<div className={styles.body}>{children} </div>
</div>
</main>
<footer className={styles.footer}>
<span>Powered by</span>
<a
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
target="_blank"
rel="noopener noreferrer"
className={styles.textLink}
>
Blitz.js
</a>
</footer>
</div>
</BlitzProvider>
</body>
</html>
)
}

49
apps/next13/app/page.tsx Normal file
View File

@@ -0,0 +1,49 @@
import Link from "next/link"
import styles from "src/styles/Home.module.css"
import Test from "./react-query"
import {cookies, headers} from "next/headers"
import {getServerSession} from "../src/blitz-server"
import getCurrentUser from "../src/users/queries/getCurrentUser"
import {SessionContext} from "@blitzjs/auth"
export default async function Home() {
const session = await getServerSession(cookies(), headers())
console.log("session", session.userId)
const user = await getCurrentUser(null, {session})
console.log("user", user)
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
width: "100%",
}}
>
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
}}
>
<Link href={"/auth/signup"} className={styles.button}>
<strong>Sign Up</strong>
</Link>
<Link href={"/auth/login"} className={styles.loginButton}>
<strong>Login</strong>
</Link>
<div style={{height: 20}} />
<p>Server Session</p>
<p>Role: {user?.role}</p>
<p>UserId: {user?.id}</p>
<p>Email: {user?.email}</p>
<div style={{height: 20}} />
<p>Client Session</p>
<Test />
</div>
</div>
)
}

View File

@@ -0,0 +1,13 @@
"use client"
import {BlitzRscProvider} from "../src/blitz-client"
type Props = {
children: Element
}
globalThis.__BLITZ_RSC = true
const BlitzProvider = ({children}: Props) => <BlitzRscProvider>{children}</BlitzRscProvider>
export default BlitzProvider

View File

@@ -0,0 +1,34 @@
"use client"
import {useQuery, useMutation} from "@blitzjs/react-query"
import logout from "../src/auth/mutations/logout"
import getCurrentUser from "../src/users/queries/getCurrentUser"
import {useTransition} from "react"
import {useRouter} from "next/navigation"
export default function Test() {
const router = useRouter()
const [user] = useQuery(getCurrentUser, null)
const [isPending, startTransition] = useTransition()
const [logoutMutation] = useMutation(logout)
console.log(user)
return (
<div>
<h1>Test</h1>
<p>{user?.email}</p>
<button
className="button small"
onClick={async () => {
await logoutMutation()
startTransition(() => {
// Refresh the current route and fetch new data from the server without
// losing client-side browser or React state.
router.refresh()
})
}}
>
Logout
</button>
</div>
)
}

View File

@@ -0,0 +1,10 @@
const {withBlitz} = require("@blitzjs/next")
/** @type {import('next').NextConfig} */
const nextConfig = {
experimental: {
appDir: true,
},
}
module.exports = withBlitz(nextConfig)

35
apps/next13/package.json Normal file
View File

@@ -0,0 +1,35 @@
{
"name": "next-blitz-auth",
"version": "0.1.0",
"private": true,
"scripts": {
"blitz:dev": "next dev",
"blitz:build": "next build",
"blitz:start": "next start",
"lint": "next lint"
},
"dependencies": {
"@blitzjs/auth": "workspace:*",
"@blitzjs/config": "workspace:*",
"@blitzjs/next": "workspace:*",
"@blitzjs/react-query": "workspace:*",
"@blitzjs/rpc": "workspace:*",
"@prisma/client": "^4.5.0",
"@tanstack/react-query": "4.0.10",
"blitz": "workspace:*",
"flatted": "3.2.7",
"next": "13.1.2",
"prisma": "^4.5.0",
"react": "18.2.0",
"react-dom": "18.2.0",
"superjson": "1.11.0"
},
"devDependencies": {
"@types/node": "18.11.7",
"@types/react": "18.0.23",
"@types/react-dom": "18.0.7",
"eslint": "8.26.0",
"eslint-config-next": "13.0.0",
"typescript": "4.8.4"
}
}

BIN
apps/next13/prisma/dev.db Normal file

Binary file not shown.

View File

@@ -0,0 +1,5 @@
import {PrismaClient} from "@prisma/client"
export * from "@prisma/client"
const db = new PrismaClient()
export default db

View File

@@ -0,0 +1,47 @@
-- CreateTable
CREATE TABLE "User" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"name" TEXT,
"email" TEXT NOT NULL,
"hashedPassword" TEXT,
"role" TEXT NOT NULL DEFAULT 'USER'
);
-- CreateTable
CREATE TABLE "Session" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"expiresAt" DATETIME,
"handle" TEXT NOT NULL,
"hashedSessionToken" TEXT,
"antiCSRFToken" TEXT,
"publicData" TEXT,
"privateData" TEXT,
"userId" INTEGER,
CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateTable
CREATE TABLE "Token" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"hashedToken" TEXT NOT NULL,
"type" TEXT NOT NULL,
"expiresAt" DATETIME NOT NULL,
"sentTo" TEXT NOT NULL,
"userId" INTEGER NOT NULL,
CONSTRAINT "Token_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session_handle_key" ON "Session"("handle");
-- CreateIndex
CREATE UNIQUE INDEX "Token_hashedToken_type_key" ON "Token"("hashedToken", "type");

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "sqlite"

View File

@@ -0,0 +1,33 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String?
email String @unique
hashedPassword String?
sessions Session[]
}
model Session {
id Int @id @default(autoincrement())
expiresAt DateTime?
handle String @unique
hashedSessionToken String?
antiCSRFToken String?
publicData String?
privateData String?
user User? @relation(fields: [userId], references: [id])
userId Int?
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -0,0 +1,4 @@
<svg width="283" height="64" viewBox="0 0 283 64" fill="none"
xmlns="http://www.w3.org/2000/svg">
<path d="M141.04 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.46 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM248.72 16c-11.04 0-19 7.2-19 18s8.96 18 20 18c6.67 0 12.55-2.64 16.19-7.09l-7.65-4.42c-2.02 2.21-5.09 3.5-8.54 3.5-4.79 0-8.86-2.5-10.37-6.5h28.02c.22-1.12.35-2.28.35-3.5 0-10.79-7.96-17.99-19-17.99zm-9.45 14.5c1.25-3.99 4.67-6.5 9.45-6.5 4.79 0 8.21 2.51 9.45 6.5h-18.9zM200.24 34c0 6 3.92 10 10 10 4.12 0 7.21-1.87 8.8-4.92l7.68 4.43c-3.18 5.3-9.14 8.49-16.48 8.49-11.05 0-19-7.2-19-18s7.96-18 19-18c7.34 0 13.29 3.19 16.48 8.49l-7.68 4.43c-1.59-3.05-4.68-4.92-8.8-4.92-6.07 0-10 4-10 10zm82.48-29v46h-9V5h9zM36.95 0L73.9 64H0L36.95 0zm92.38 5l-27.71 48L73.91 5H84.3l17.32 30 17.32-30h10.39zm58.91 12v9.69c-1-.29-2.06-.49-3.2-.49-5.81 0-10 4-10 10V51h-9V17h9v9.2c0-5.08 5.91-9.2 13.2-9.2z" fill="#000"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,53 @@
import {AuthenticationError, PromiseReturnType} from "blitz"
import Link from "next/link"
import {LabeledTextField} from "../../core/components/LabeledTextField"
import {Form, FORM_ERROR} from "../../core/components/Form"
import login from "../../auth/mutations/login"
import {Login} from "../../auth/validations"
import {useMutation} from "@blitzjs/react-query"
type LoginFormProps = {
onSuccess?: (user: PromiseReturnType<typeof login>) => void
}
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
<Form
submitText="Login"
schema={Login}
initialValues={{email: "", password: ""}}
onSubmit={async (values) => {
try {
const user = await loginMutation(values)
props.onSuccess?.(user)
} catch (error: any) {
if (error instanceof AuthenticationError) {
return {[FORM_ERROR]: "Sorry, those credentials are invalid"}
} else {
return {
[FORM_ERROR]:
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
}
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
<div>
<a>Forgot your password?</a>
</div>
</Form>
<div style={{marginTop: "1rem"}}>
Or <Link href={"/auth/signup"}>Sign Up</Link>
</div>
</div>
)
}
export default LoginForm

View File

@@ -0,0 +1,42 @@
import {LabeledTextField} from "../../core/components/LabeledTextField"
import {Form, FORM_ERROR} from "../../core/components/Form"
import signup from "../../auth/mutations/signup"
import {Signup} from "../../auth/validations"
import {useMutation} from "@blitzjs/react-query"
type SignupFormProps = {
onSuccess?: () => void
}
export const SignupForm = (props: SignupFormProps) => {
const [signupMutation] = useMutation(signup)
return (
<div>
<h1>Create an Account</h1>
<Form
submitText="Create Account"
schema={Signup}
initialValues={{email: "", password: ""}}
onSubmit={async (values) => {
try {
await signupMutation(values)
props.onSuccess?.()
} catch (error: any) {
if (error.code === "P2002" && error.meta?.target?.includes("email")) {
// Error "P2002" comes from Prisma (https://www.prisma.io/docs/reference/api-reference/error-reference#p2002)
return {email: "This email is already being used"}
} else {
return {[FORM_ERROR]: error.toString()}
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
</Form>
</div>
)
}
export default SignupForm

View File

@@ -0,0 +1,25 @@
import {NotFoundError} from "blitz"
import db from "../../../prisma"
import {authenticateUser} from "./login"
import {ChangePassword} from "../validations"
import {resolver} from "@blitzjs/rpc"
// import {SecurePassword} from "@blitzjs/auth"
export default resolver.pipe(
resolver.zod(ChangePassword),
resolver.authorize(),
async ({currentPassword, newPassword}, ctx) => {
const user = await db.user.findFirst({where: {id: ctx.session.userId}})
if (!user) throw new NotFoundError()
await authenticateUser(user.email, currentPassword)
// const hashedPassword = await SecurePassword.hash(newPassword.trim())
await db.user.update({
where: {id: user.id},
data: {hashedPassword: newPassword},
})
return true
},
)

View File

@@ -0,0 +1,64 @@
import {vi, describe, it, beforeEach} from "vitest"
import db from "db"
import {hash256} from "@blitzjs/auth"
import forgotPassword from "./forgotPassword"
import previewEmail from "preview-email"
import {Ctx} from "@blitzjs/next"
beforeEach(async () => {
await db.$reset()
})
const generatedToken = "plain-token"
vi.mock("@blitzjs/auth", async () => {
const auth = await vi.importActual<Record<string, unknown>>("@blitzjs/auth")!
return {
...auth,
generateToken: () => generatedToken,
}
})
vi.mock("preview-email", () => ({default: vi.fn()}))
describe("forgotPassword mutation", () => {
it("does not throw error if user doesn't exist", async () => {
await expect(forgotPassword({email: "no-user@email.com"}, {} as Ctx)).resolves.not.toThrow()
})
it("works correctly", async () => {
// Create test user
const user = await db.user.create({
data: {
email: "user@example.com",
tokens: {
// Create old token to ensure it's deleted
create: {
type: "RESET_PASSWORD",
hashedToken: "token",
expiresAt: new Date(),
sentTo: "user@example.com",
},
},
},
include: {tokens: true},
})
// Invoke the mutation
await forgotPassword({email: user.email}, {} as Ctx)
const tokens = await db.token.findMany({where: {userId: user.id}})
const token = tokens[0]
if (!user.tokens[0]) throw new Error("Missing user token")
if (!token) throw new Error("Missing token")
// delete's existing tokens
expect(tokens.length).toBe(1)
expect(token.id).not.toBe(user.tokens[0].id)
expect(token.type).toBe("RESET_PASSWORD")
expect(token.sentTo).toBe(user.email)
expect(token.hashedToken).toBe(hash256(generatedToken))
expect(token.expiresAt > new Date()).toBe(true)
expect(previewEmail).toBeCalled()
})
})

View File

@@ -0,0 +1,40 @@
import {generateToken, hash256} from "@blitzjs/auth"
import {resolver} from "@blitzjs/rpc"
import db from "../../../prisma"
import {ForgotPassword} from "../validations"
const RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS = 4
export default resolver.pipe(resolver.zod(ForgotPassword), async ({email}) => {
// 1. Get the user
const user = await db.user.findFirst({where: {email: email.toLowerCase()}})
// 2. Generate the token and expiration date.
const token = generateToken()
const hashedToken = hash256(token)
const expiresAt = new Date()
expiresAt.setHours(expiresAt.getHours() + RESET_PASSWORD_TOKEN_EXPIRATION_IN_HOURS)
// 3. If user with this email was found
if (user) {
// 4. Delete any existing password reset tokens
await db.token.deleteMany({where: {type: "RESET_PASSWORD", userId: user.id}})
// 5. Save this new token in the database.
await db.token.create({
data: {
user: {connect: {id: user.id}},
type: "RESET_PASSWORD",
expiresAt,
hashedToken,
sentTo: user.email,
},
})
// 6. Send the email
} else {
// 7. If no user found wait the same time so attackers can't tell the difference
await new Promise((resolve) => setTimeout(resolve, 750))
}
// 8. Return the same result whether a password reset email was sent or not
return
})

View File

@@ -0,0 +1,31 @@
import {resolver} from "@blitzjs/rpc"
import {AuthenticationError} from "blitz"
import db from "../../../prisma"
import {Login} from "../validations"
export const authenticateUser = async (rawEmail: string, rawPassword: string) => {
const {email, password} = Login.parse({email: rawEmail, password: rawPassword})
const user = await db.user.findFirst({where: {email}})
if (!user) throw new AuthenticationError()
const result = await SecurePassword.verify(user.hashedPassword, password)
if (result === SecurePassword.VALID_NEEDS_REHASH) {
// Upgrade hashed password with a more secure hash
const improvedHash = await SecurePassword.hash(password)
await db.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}})
}
const {hashedPassword, ...rest} = user
return rest
}
export default resolver.pipe(resolver.zod(Login), async ({email, password}, ctx) => {
// This throws an error if credentials are invalid
// const user = await authenticateUser(email, password)
const user = await db.user.findFirst({where: {email}})
//@ts-ignore
await ctx.session.$create({userId: user.id, role: user.role})
console.log("user", user)
return user
})

View File

@@ -0,0 +1,5 @@
import {Ctx} from "blitz"
export default async function logout(_: any, ctx: Ctx) {
return await ctx.session.$revoke()
}

View File

@@ -0,0 +1,83 @@
import {vi, describe, it, beforeEach, expect} from "vitest"
import resetPassword from "./resetPassword"
import db from "db"
import {SecurePassword, hash256} from "@blitzjs/auth"
beforeEach(async () => {
await db.$reset()
})
const mockCtx: any = {
session: {
$create: vi.fn(),
},
}
describe("resetPassword mutation", () => {
it("works correctly", async () => {
expect(true).toBe(true)
// Create test user
const goodToken = "randomPasswordResetToken"
const expiredToken = "expiredRandomPasswordResetToken"
const future = new Date()
future.setHours(future.getHours() + 4)
const past = new Date()
past.setHours(past.getHours() - 4)
const user = await db.user.create({
data: {
email: "user@example.com",
tokens: {
// Create old token to ensure it's deleted
create: [
{
type: "RESET_PASSWORD",
hashedToken: hash256(expiredToken),
expiresAt: past,
sentTo: "user@example.com",
},
{
type: "RESET_PASSWORD",
hashedToken: hash256(goodToken),
expiresAt: future,
sentTo: "user@example.com",
},
],
},
},
include: {tokens: true},
})
const newPassword = "newPassword"
// Non-existent token
await expect(
resetPassword({token: "no-token", password: "", passwordConfirmation: ""}, mockCtx),
).rejects.toThrowError()
// Expired token
await expect(
resetPassword(
{token: expiredToken, password: newPassword, passwordConfirmation: newPassword},
mockCtx,
),
).rejects.toThrowError()
// Good token
await resetPassword(
{token: goodToken, password: newPassword, passwordConfirmation: newPassword},
mockCtx,
)
// Delete's the token
const numberOfTokens = await db.token.count({where: {userId: user.id}})
expect(numberOfTokens).toBe(0)
// Updates user's password
const updatedUser = await db.user.findFirst({where: {id: user.id}})
expect(await SecurePassword.verify(updatedUser!.hashedPassword, newPassword)).toBe(
SecurePassword.VALID,
)
})
})

View File

@@ -0,0 +1,50 @@
import {hash256} from "@blitzjs/auth"
import db from "../../../prisma"
import {ResetPassword} from "../validations"
import login from "./login"
export class ResetPasswordError extends Error {
name = "ResetPasswordError"
message = "Reset password link is invalid or it has expired."
}
export default async function resetPassword(input: any, ctx: any) {
ResetPassword.parse(input)
// 1. Try to find this token in the database
const hashedToken = hash256(input.token)
const possibleToken = await db.token.findFirst({
where: {hashedToken, type: "RESET_PASSWORD"},
include: {user: true},
})
// 2. If token not found, error
if (!possibleToken) {
throw new ResetPasswordError()
}
const savedToken = possibleToken
// 3. Delete token so it can't be used again
await db.token.delete({where: {id: savedToken.id}})
// 4. If token has expired, error
if (savedToken.expiresAt < new Date()) {
throw new ResetPasswordError()
}
// 5. Since token is valid, now we can update the user's password
// const hashedPassword = await SecurePassword.hash(input.password.trim())
const user = await db.user.update({
where: {id: savedToken.userId},
data: {
hashedPassword: input.password,
},
})
// 6. Revoke all existing login sessions for this user
await db.session.deleteMany({where: {userId: user.id}})
// 7. Now log the user in with the new credentials
await login({email: user.email, password: input.password}, ctx)
return true
}

View File

@@ -0,0 +1,20 @@
import db from "../../../prisma"
// import {SecurePassword} from "@blitzjs/auth"
export default async function signup(input: {password: string; email: string}, ctx: any) {
const blitzContext = ctx
// const hashedPassword = await SecurePassword.hash((input.password as string) || "test-password")
const hashedPassword = (input.password as string) || "test-password"
const email = (input.email as string) || "test" + Math.random() + "@test.com"
const user = await db.user.create({
data: {email, hashedPassword, role: "user"},
select: {id: true, name: true, email: true, role: true},
})
await blitzContext.session.$create({
userId: user.id,
role: user.role,
})
return {userId: blitzContext.session.userId, ...user, email: input.email}
}

View File

@@ -0,0 +1,42 @@
import {z} from "zod"
export const email = z
.string()
.email()
.transform((str) => str.toLowerCase().trim())
export const password = z
.string()
.min(10)
.max(100)
.transform((str) => str.trim())
export const Signup = z.object({
email,
password,
})
export const Login = z.object({
email,
password: z.string(),
})
export const ForgotPassword = z.object({
email,
})
export const ResetPassword = z
.object({
password: password,
passwordConfirmation: password,
token: z.string(),
})
.refine((data) => data.password === data.passwordConfirmation, {
message: "Passwords don't match",
path: ["passwordConfirmation"], // set the path of the error
})
export const ChangePassword = z.object({
currentPassword: z.string(),
newPassword: password,
})

View File

@@ -0,0 +1,13 @@
"use client"
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupBlitzClient} from "@blitzjs/next"
import {BlitzReactQueryPlugin} from "@blitzjs/react-query"
export const {withBlitz, useSession, queryClient, BlitzRscProvider} = setupBlitzClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "web-cookie-prefix",
}),
BlitzReactQueryPlugin({}),
],
})

View File

@@ -0,0 +1,23 @@
import type {BlitzCliConfig} from "blitz"
import {setupBlitzServer} from "@blitzjs/next"
import {AuthServerPlugin, PrismaStorage} from "@blitzjs/auth"
import db from "../prisma"
import {simpleRolesIsAuthorized} from "@blitzjs/auth"
import {BlitzLogger} from "blitz"
const {gSSP, gSP, api, getServerSession} = setupBlitzServer({
plugins: [
AuthServerPlugin({
cookiePrefix: "web-cookie-prefix",
storage: PrismaStorage(db),
isAuthorized: simpleRolesIsAuthorized,
}),
],
logger: BlitzLogger({}),
})
export {gSSP, gSP, api, getServerSession}
export const cliConfig: BlitzCliConfig = {
customTemplates: "src/templates",
}

View File

@@ -0,0 +1,83 @@
import {useState, ReactNode, PropsWithoutRef} from "react"
import {FormProvider, useForm, UseFormProps} from "react-hook-form"
import {zodResolver} from "@hookform/resolvers/zod"
import {z} from "zod"
export interface FormProps<S extends z.ZodType<any, any>>
extends Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit"> {
/** All your form fields */
children?: ReactNode
/** Text to display in the submit button */
submitText?: string
schema?: S
onSubmit: (values: z.infer<S>) => Promise<void | OnSubmitResult>
initialValues?: UseFormProps<z.infer<S>>["defaultValues"]
}
interface OnSubmitResult {
FORM_ERROR?: string
[prop: string]: any
}
export const FORM_ERROR = "FORM_ERROR"
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
const ctx = useForm<z.infer<S>>({
mode: "onBlur",
resolver: schema ? zodResolver(schema) : undefined,
defaultValues: initialValues,
})
const [formError, setFormError] = useState<string | null>(null)
return (
<FormProvider {...ctx}>
<form
onSubmit={ctx.handleSubmit(async (values) => {
const result = (await onSubmit(values)) || {}
for (const [key, value] of Object.entries(result)) {
if (key === FORM_ERROR) {
setFormError(value)
} else {
ctx.setError(key as any, {
type: "submit",
message: value,
})
}
}
})}
className="form"
{...props}
>
{/* Form fields supplied as children are rendered here */}
{children}
{formError && (
<div role="alert" style={{color: "red"}}>
{formError}
</div>
)}
{submitText && (
<button type="submit" disabled={ctx.formState.isSubmitting}>
{submitText}
</button>
)}
<style global jsx>{`
.form > * + * {
margin-top: 1rem;
}
`}</style>
</form>
</FormProvider>
)
}
export default Form

View File

@@ -0,0 +1,61 @@
import {forwardRef, PropsWithoutRef, ComponentPropsWithoutRef} from "react"
import {useFormContext} from "react-hook-form"
import {ErrorMessage} from "@hookform/error-message"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
/** Field name. */
name: string
/** Field label. */
label: string
/** Field type. Doesn't include radio buttons and checkboxes */
type?: "text" | "password" | "email" | "number"
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
labelProps?: ComponentPropsWithoutRef<"label">
}
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({label, outerProps, labelProps, name, ...props}, ref) => {
const {
register,
formState: {isSubmitting, errors},
} = useFormContext()
return (
<div {...outerProps}>
<label {...labelProps}>
{label}
<input disabled={isSubmitting} {...register(name)} {...props} />
</label>
<ErrorMessage
render={({message}) => (
<div role="alert" style={{color: "red"}}>
{message}
</div>
)}
errors={errors}
name={name}
/>
<style jsx>{`
label {
display: flex;
flex-direction: column;
align-items: start;
font-size: 1rem;
}
input {
font-size: 1rem;
padding: 0.25rem 0.5rem;
border-radius: 3px;
border: 1px solid purple;
appearance: none;
margin-top: 0.5rem;
}
`}</style>
</div>
)
},
)
export default LabeledTextField

View File

@@ -0,0 +1,8 @@
import {AppProps} from "@blitzjs/next"
import {withBlitz} from "../blitz-client"
function MyApp({Component, pageProps}: AppProps) {
return <Component {...pageProps} />
}
export default withBlitz(MyApp, true)

View File

@@ -0,0 +1,8 @@
import {api} from "../../../blitz-server"
export default api((req, res, ctx) => {
// console.log("session", ctx.session)
//get cookie
console.dir("cookie", req.headers.cookie)
res.json({session: ctx.session.userId})
})

View File

@@ -0,0 +1,4 @@
import {rpcHandler} from "@blitzjs/rpc"
import {api} from "src/blitz-server"
export default api(rpcHandler({onError: console.log}))

View File

@@ -0,0 +1,303 @@
.container {
display: flex;
flex-direction: column;
height: 100vh;
}
.main {
flex: 1;
padding: 0rem 1rem;
display: flex;
flex-direction: column;
}
.wrapper {
display: flex;
flex-direction: column;
gap: 2rem;
}
.frost {
background: rgba(255, 255, 255, 0.2);
border-radius: 16px;
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
border: 1px solid rgba(255, 255, 255, 0.31);
}
.header {
display: flex;
flex-direction: column;
padding: 4rem 0rem 2rem 0rem;
text-align: center;
gap: 2rem;
}
.header h1 {
max-width: 620px;
align-self: center;
}
.body {
composes: frost;
border-radius: var(--border-radius);
display: flex;
padding: 1rem;
width: 100%;
flex-direction: row;
align-self: center;
max-width: var(--screen-width);
}
.instructions {
display: inline-flex;
flex-direction: column;
padding: 1rem;
width: 100%;
}
.globe {
position: fixed;
width: 350vmin;
height: 75vmin;
left: 20%;
top: 50%;
transform: translate(-50%, calc(-50% + 40px));
z-index: -1;
border-radius: 100%;
background-image: radial-gradient(
95.63% 95.63% at 95.92% 0%,
rgba(255, 255, 255, 0.62) 0%,
#8155ff38 60.42%,
#002fff5c 169%
);
filter: blur(8vmin);
}
.footer {
display: flex;
padding: 2rem 0;
justify-content: center;
align-items: center;
}
.footer span {
margin-right: 0.2rem;
}
.code {
display: flex;
align-items: center;
gap: 0.5rem;
}
.code span {
background: rgba(124, 58, 237, 50%);
border-radius: 50rem;
font-size: 14px;
font-weight: 500;
padding: 17px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
color: rgb(57, 33, 97);
}
.code pre {
background: rgba(124, 58, 237, 12%);
border-radius: 4px;
padding: 0.7em 1.4em;
text-align: center;
}
.code code {
font-size: 0.86em;
font-weight: bold;
color: rgb(124, 58, 237);
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
Bitstream Vera Sans Mono, Courier New, monospace;
}
.toastContainer {
border: 1px solid #edff;
padding: 0 1rem;
background: #eeff;
color: #62af;
text-align: center;
}
.toastContainer strong {
color: rgb(124, 58, 237);
}
.textLink {
color: rgb(124, 58, 237);
background: linear-gradient(to right, rgba(231, 216, 246, 1), rgba(231, 216, 246, 1)),
linear-gradient(to right, rgba(99, 1, 235, 1), rgba(124, 58, 237, 1), rgba(231, 216, 246, 1));
background-size: 100% 1px, 0 1px;
background-position: 100% 100%, 0 100%;
background-repeat: no-repeat;
transition: background-size 400ms;
}
.textLink:hover,
.textLink:focus,
.textLink:active {
background-size: 0 1px, 100% 1px;
}
.arrowIcon {
box-sizing: border-box;
display: block;
width: 8px;
height: 8px;
border-top: 2px solid;
transform: scale(var(--ggs, 1));
border-right: 2px solid;
position: absolute;
right: 6px;
top: 6px;
color: #b1a5c4;
}
.arrowIcon::after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 2px;
background: currentColor;
transform: rotate(-45deg);
top: 2px;
right: -1px;
}
.buttonContainer {
display: flex;
flex-direction: row;
gap: 1rem;
justify-content: center;
align-items: center;
flex: 1;
}
.button {
background: linear-gradient(to top, rgb(124, 58, 237), rgb(117, 81, 236));
border: 1px solid rgb(231, 216, 246);
color: white;
text-shadow: rgba(0, 0, 0, 0.25) 0px 3px 8px;
padding: 0 24px;
height: 48px;
width: 200px;
max-width: 300px;
position: relative;
display: inline-flex;
justify-content: center;
align-items: center;
flex-shrink: 0;
user-select: none;
white-space: nowrap;
border-radius: 0.75rem;
border-bottom-left-radius: 0px;
font-size: 15px;
transition: all 0.3s ease 0s;
cursor: pointer;
}
.button:hover {
color: white;
text-shadow: rgb(0 0 0 / 56%) 0px 3px 12px;
box-shadow: rgb(80 63 205 / 50%) 0px 1px 40px;
}
.loginButton {
composes: button;
background: rgb(248 250 252);
border: 1px solid rgb(231, 216, 246);
color: rgb(30 41 59);
text-shadow: none;
}
.loginButton:hover {
color: rgb(30 41 59);
text-shadow: none;
}
.card:hover .arrowIcon {
color: #7450ec;
}
.linkGrid {
display: flex;
flex-direction: column;
flex-wrap: wrap;
gap: 1rem;
}
.card {
composes: frost;
padding: 1rem 0rem;
text-align: center;
color: inherit;
text-decoration: none;
border-radius: 10px;
border-bottom-left-radius: 0px;
transition: color 0.15s ease, border-color 0.15s ease;
max-width: 200px;
min-width: 200px;
display: flex;
flex-direction: row;
justify-content: center;
}
.card:hover,
.card:focus,
.card:active {
color: #7450ec;
border-color: #7450ec;
}
.card h2 {
margin: 0 0 1rem 0;
font-size: 1.5rem;
}
.card p {
margin: 0;
font-size: 1.25rem;
line-height: 1.5;
}
.logo {
flex: 1;
padding: 1rem 2rem;
}
.logo svg {
height: 100%;
width: 200px;
fill: #7450ec;
}
/* MOBILE */
@media (max-width: 800px) {
.linkGrid {
width: 100%;
}
.card {
max-width: 100%;
}
.body {
flex-wrap: wrap;
}
.buttonContainer {
flex-wrap: wrap;
}
}

View File

@@ -0,0 +1,25 @@
:root {
--border-radius: 0.75rem;
--screen-width: 90vmin;
}
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen, Ubuntu, Cantarell,
Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
i {
font-size: 0.8rem;
}
* {
box-sizing: border-box;
}

View File

@@ -0,0 +1,7 @@
import {useQuery} from "@blitzjs/react-query"
import getCurrentUser from "../../../src/users/queries/getCurrentUser"
export const useCurrentUser = () => {
const [user] = useQuery(getCurrentUser, null)
return user
}

View File

@@ -0,0 +1,16 @@
import {Ctx} from "blitz"
import db from "../../../prisma"
export default async function getCurrentUser(input: null, ctx: Ctx) {
if (!ctx.session.userId) return null
const user = await db.user.findFirst({
where: {id: ctx.session.userId},
select: {id: true, name: true, email: true, role: true},
})
return user
}
export const config = {
httpMethod: "GET",
}

25
apps/next13/tsconfig.json Normal file
View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
]
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

12
apps/next13/types.ts Normal file
View File

@@ -0,0 +1,12 @@
import {SimpleRolesIsAuthorized} from "@blitzjs/auth"
import {User} from "./prisma"
declare module "@blitzjs/auth" {
export interface Session {
isAuthorized: SimpleRolesIsAuthorized
PublicData: {
userId: User["id"]
email: User["email"]
}
}
}

View File

@@ -27,6 +27,7 @@
"@blitzjs/auth": "workspace:*",
"@blitzjs/config": "workspace:*",
"@blitzjs/next": "workspace:*",
"@blitzjs/react-query": "workspace:*",
"@blitzjs/rpc": "workspace:*",
"@hookform/error-message": "2.0.0",
"@hookform/resolvers": "2.9.10",

View File

@@ -4,7 +4,7 @@ import { LabeledTextField } from "src/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "src/core/components/Form"
import login from "src/auth/mutations/login"
import { Login } from "src/auth/validations"
import { useMutation } from "@blitzjs/rpc"
import { useMutation } from "@blitzjs/react-query"
import { Routes } from "@blitzjs/next"
type LoginFormProps = {

View File

@@ -2,7 +2,7 @@ import { LabeledTextField } from "src/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "src/core/components/Form"
import signup from "src/auth/mutations/signup"
import { Signup } from "src/auth/validations"
import { useMutation } from "@blitzjs/rpc"
import { useMutation } from "@blitzjs/react-query"
type SignupFormProps = {
onSuccess?: () => void

View File

@@ -24,8 +24,9 @@ export const authenticateUser = async (rawEmail: string, rawPassword: string) =>
export default resolver.pipe(resolver.zod(Login), async ({ email, password }, ctx) => {
// This throws an error if credentials are invalid
const user = await authenticateUser(email, password)
// const user = await authenticateUser(email, password)
const user = (await db.user.findFirst({ where: { email } })) as any
if (!user) throw new AuthenticationError()
await ctx.session.$create({ userId: user.id, role: user.role as Role })
return user

View File

@@ -1,6 +1,6 @@
import { AuthClientPlugin } from "@blitzjs/auth"
import { setupBlitzClient } from "@blitzjs/next"
import { BlitzRpcPlugin } from "@blitzjs/rpc"
import { BlitzReactQueryPlugin } from "@blitzjs/react-query"
import { BlitzCustomPlugin } from "./custom-plugin/plugin"
export const { withBlitz } = setupBlitzClient({
@@ -8,7 +8,7 @@ export const { withBlitz } = setupBlitzClient({
AuthClientPlugin({
cookiePrefix: "web-cookie-prefix",
}),
BlitzRpcPlugin({}),
BlitzReactQueryPlugin({}),
BlitzCustomPlugin({}),
],
})

View File

@@ -3,7 +3,7 @@ import { LabeledTextField } from "src/core/components/LabeledTextField"
import { Form, FORM_ERROR } from "src/core/components/Form"
import { ForgotPassword } from "src/auth/validations"
import forgotPassword from "src/auth/mutations/forgotPassword"
import { useMutation } from "@blitzjs/rpc"
import { useMutation } from "@blitzjs/react-query"
import { BlitzPage } from "@blitzjs/next"
const ForgotPasswordPage: BlitzPage = () => {

View File

@@ -6,7 +6,7 @@ import { ResetPassword } from "src/auth/validations"
import resetPassword from "src/auth/mutations/resetPassword"
import { BlitzPage, Routes } from "@blitzjs/next"
import { useRouter } from "next/router"
import { useMutation } from "@blitzjs/rpc"
import { useMutation } from "@blitzjs/react-query"
import Link from "next/link"
const ResetPasswordPage: BlitzPage = () => {

View File

@@ -5,8 +5,8 @@ import Layout from "src/core/layouts/Layout"
import { useCurrentUser } from "src/users/hooks/useCurrentUser"
import logout from "src/auth/mutations/logout"
import logo from "public/logo.png"
import { useMutation } from "@blitzjs/rpc"
import { Routes, BlitzPage } from "@blitzjs/next"
import { useMutation } from "@blitzjs/react-query"
import { Routes, BlitzPage, useParams } from "@blitzjs/next"
/*
* This file is just for a pleasant getting started page for your new app.

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@blitzjs/rpc"
import { useQuery } from "@blitzjs/react-query"
import getCurrentUser from "src/users/queries/getCurrentUser"
export const useCurrentUser = () => {

View File

@@ -3,7 +3,7 @@ import { render as defaultRender } from "@testing-library/react"
import { renderHook as defaultRenderHook } from "@testing-library/react-hooks"
import { NextRouter } from "next/router"
import { BlitzProvider, RouterContext } from "@blitzjs/next"
import { QueryClient } from "@blitzjs/rpc"
import { QueryClient } from "@blitzjs/react-query"
export * from "@testing-library/react"

View File

@@ -2,7 +2,7 @@ import type {BlitzServerPlugin, RequestMiddleware, Ctx} from "blitz"
import {assert} from "blitz"
import {IncomingMessage, ServerResponse} from "http"
import {PublicData, SessionModel, SessionConfigMethods} from "../shared/types"
import {getSession} from "./auth-sessions"
import {getServerSession, getSession} from "./auth-sessions"
interface SessionConfigOptions {
cookiePrefix?: string
@@ -85,6 +85,12 @@ export function AuthServerPlugin(options: AuthPluginOptions): BlitzServerPlugin<
// pass types
globalThis.__BLITZ_SESSION_COOKIE_PREFIX = options.cookiePrefix || "blitz"
globalThis.sessionConfig = {
...defaultConfig_,
...options.storage,
...options,
}
function authPluginSessionMiddleware() {
assert(
options.isAuthorized,
@@ -121,5 +127,8 @@ export function AuthServerPlugin(options: AuthPluginOptions): BlitzServerPlugin<
}
return {
requestMiddlewares: [authPluginSessionMiddleware()],
exports: () => ({
getServerSession,
}),
}
}

View File

@@ -35,6 +35,8 @@ import {
AuthenticatedSessionContext,
} from "../shared"
import {generateToken, hash256} from "./auth-utils"
import {Socket} from "net"
import {getAntiCSRFToken} from "../client"
export function isLocalhost(req: any): boolean {
let {host} = req.headers
@@ -150,6 +152,7 @@ export async function getSession(
debug("cookiePrefix", globalThis.__BLITZ_SESSION_COOKIE_PREFIX)
if (res.blitzCtx.session) {
debug("Returning existing session")
return res.blitzCtx.session
}
@@ -165,10 +168,24 @@ export async function getSession(
}
const sessionContext = makeProxyToPublicData(new SessionContextClass(req, res, sessionKernel))
debug("New session context")
res.blitzCtx.session = sessionContext
return sessionContext
}
export async function getServerSession(_cookies: any, _headers: Headers): Promise<SessionContext> {
const req = new IncomingMessage(new Socket()) as IncomingMessage & {
cookies: {[key: string]: string}
}
req.headers = Object.fromEntries(_headers as any)
req.headers[HEADER_CSRF] = _cookies.get(COOKIE_CSRF_TOKEN()).value
req.cookies = Object.fromEntries(
_cookies.getAll().map((c: {name: string; value: string}) => [c.name, c.value]),
)
const res = new ServerResponse(req)
return await getSession(req, res)
}
const makeProxyToPublicData = <T extends SessionContextClass>(ctxClass: T): T => {
return new Proxy(ctxClass, {
get(target, prop, receiver) {

View File

@@ -1,6 +1,6 @@
import * as crypto from "crypto"
import {nanoid} from "nanoid"
import SecurePasswordLib from "secure-password"
// import SecurePasswordLib from "secure-password"
import {AuthenticationError} from "blitz"
export const hash256 = (input: string = "") => {
@@ -9,35 +9,35 @@ export const hash256 = (input: string = "") => {
export const generateToken = (numberOfCharacters: number = 32) => nanoid(numberOfCharacters)
const SP = () => new SecurePasswordLib()
// const SP = () => new SecurePasswordLib()
export const SecurePassword = {
...SecurePasswordLib,
async hash(password: string | null | undefined) {
if (!password) {
throw new AuthenticationError()
}
const hashedBuffer = await SP().hash(Buffer.from(password))
return hashedBuffer.toString("base64")
},
async verify(hashedPassword: string | null | undefined, password: string | null | undefined) {
if (!hashedPassword || !password) {
throw new AuthenticationError()
}
try {
const result = await SP().verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
// Return result for valid results.
switch (result) {
case SecurePassword.VALID:
case SecurePassword.VALID_NEEDS_REHASH:
return result
default:
// For everything else throw AuthenticationError
throw new AuthenticationError()
}
} catch (error) {
// Could be error like failed to hash password
throw new AuthenticationError()
}
},
}
// export const SecurePassword = {
// ...SecurePasswordLib,
// async hash(password: string | null | undefined) {
// if (!password) {
// throw new AuthenticationError()
// }
// const hashedBuffer = await SP().hash(Buffer.from(password))
// return hashedBuffer.toString("base64")
// },
// async verify(hashedPassword: string | null | undefined, password: string | null | undefined) {
// if (!hashedPassword || !password) {
// throw new AuthenticationError()
// }
// try {
// const result = await SP().verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
// // Return result for valid results.
// switch (result) {
// case SecurePassword.VALID:
// case SecurePassword.VALID_NEEDS_REHASH:
// return result
// default:
// // For everything else throw AuthenticationError
// throw new AuthenticationError()
// }
// } catch (error) {
// // Could be error like failed to hash password
// throw new AuthenticationError()
// }
// },
// }

View File

@@ -2,7 +2,7 @@
"name": "@blitzjs/next",
"version": "2.0.0-beta.22",
"scripts": {
"build": "unbuild",
"build": "unbuild && node ./utils/add-directives.js",
"dev": "pnpm predev && pnpm watch unbuild src --wait=0.2",
"predev": "wait-on -d 250 ../blitz/dist/index-server.d.ts && wait-on -d 250 ../blitz-rpc/dist/index-server.d.ts",
"lint": "eslint . --fix",
@@ -24,6 +24,7 @@
"eslint.js"
],
"dependencies": {
"@blitzjs/react-query": "2.0.0-beta.22",
"@blitzjs/rpc": "2.0.0-beta.22",
"@tanstack/react-query": "4.0.10",
"@types/hoist-non-react-statics": "3.3.1",

View File

@@ -1,5 +1,5 @@
import {RedirectError} from "blitz"
import {Router} from "next/router"
import type {Router} from "next/router"
import * as React from "react"
import {RouterContext} from "./router-context"
import _debug from "debug"

View File

@@ -1,4 +1,4 @@
import {QueryClient} from "@tanstack/react-query"
import {QueryClient} from "@blitzjs/react-query"
declare global {
var queryClient: QueryClient

View File

@@ -1,24 +1,19 @@
import "./global"
import type {ClientPlugin, BlitzPluginWithProvider} from "blitz"
import {reduceBlitzPlugins, Ctx} from "blitz"
import {reduceBlitzClientPlugins, Ctx} from "blitz"
import Head from "next/head"
import React, {ReactNode} from "react"
import {QueryClient, QueryClientProvider, Hydrate, HydrateOptions} from "@tanstack/react-query"
import {QueryClient, QueryClientProvider, Hydrate, HydrateOptions} from "@blitzjs/react-query"
import {withSuperJSONPage} from "./superjson"
import {UrlObject} from "url"
import {AppPropsType} from "next/dist/shared/lib/utils"
import {Router, useRouter} from "next/router"
import {RouterContext} from "./router-context"
import type {Router} from "next/router"
export * from "./error-boundary"
export * from "./error-component"
export * from "./use-params"
export * from "./router-context"
export {Routes} from ".blitz"
const buildWithBlitz = (withPlugins: BlitzPluginWithProvider) => {
return function withBlitzAppRoot(UserAppRoot: React.ComponentType<AppProps>) {
const BlitzOuterRoot = (props: AppProps) => {
return function withBlitzAppRoot(UserAppRoot: React.ComponentType<AppProps>, isRSC = false) {
const BlitzOuterRoot = (props: AppProps<{dehydratedState: unknown}>) => {
const component = React.useMemo(() => withPlugins(props.Component), [props.Component])
React.useEffect(() => {
@@ -28,6 +23,10 @@ const buildWithBlitz = (withPlugins: BlitzPluginWithProvider) => {
})
}, [])
if (isRSC) {
globalThis.__BLITZ_RSC = true
return <UserAppRoot {...props} Component={component} />
}
return (
<BlitzProvider dehydratedState={props.pageProps?.dehydratedState}>
<>
@@ -80,24 +79,20 @@ export const BlitzProvider = ({
hydrateOptions,
children,
}: BlitzProviderProps) => {
const router = useRouter()
if (client) {
return (
<RouterContext.Provider value={router}>
<QueryClientProvider
client={client || globalThis.queryClient}
contextSharing={contextSharing}
>
<Hydrate state={dehydratedState} options={hydrateOptions}>
{children}
</Hydrate>
</QueryClientProvider>
</RouterContext.Provider>
<QueryClientProvider
client={client || globalThis.queryClient}
contextSharing={contextSharing}
>
<Hydrate state={dehydratedState} options={hydrateOptions}>
{children}
</Hydrate>
</QueryClientProvider>
)
}
return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>
return children
}
const setupBlitzClient = <TPlugins extends readonly ClientPlugin<object>[]>({
@@ -118,7 +113,7 @@ const setupBlitzClient = <TPlugins extends readonly ClientPlugin<object>[]>({
// allMiddleware.push(middleware)
// }
const {exports, withPlugins} = reduceBlitzPlugins({plugins})
const {exports, withPlugins} = reduceBlitzClientPlugins({plugins})
const withBlitz = buildWithBlitz(withPlugins)
@@ -167,3 +162,8 @@ export const NoPageFlicker = () => {
</Head>
)
}
export * from "./error-boundary"
export * from "./error-component"
export * from "./use-params"
export * from "./router-context"

View File

@@ -19,21 +19,16 @@ import {
initializeLogger,
} from "blitz"
import {handleRequestWithMiddleware, startWatcher, stopWatcher} from "blitz"
import {
dehydrate,
getInfiniteQueryKey,
getQueryKey,
installWebpackConfig,
InstallWebpackConfigOptions,
ResolverPathOptions,
} from "@blitzjs/rpc"
import {DefaultOptions, QueryClient} from "@tanstack/react-query"
import {installWebpackConfig, InstallWebpackConfigOptions, ResolverPathOptions} from "@blitzjs/rpc"
import {dehydrate, getInfiniteQueryKey, getQueryKey} from "@blitzjs/react-query"
import {DefaultOptions, QueryClient} from "@blitzjs/react-query"
import {IncomingMessage, ServerResponse} from "http"
import {withSuperJsonProps} from "./superjson"
import {ParsedUrlQuery} from "querystring"
import {PreviewData} from "next/types"
import {resolveHref} from "next/dist/shared/lib/router/router"
import {RouteUrlObject, isRouteUrlObject} from "blitz"
import {ServerPluginsExports} from "blitz"
export * from "./index-browser"
@@ -49,8 +44,8 @@ export type NextApiHandler<TResult> = (
res: BlitzNextApiResponse,
) => TResult | void | Promise<TResult | void>
type SetupBlitzOptions = {
plugins: BlitzServerPlugin<RequestMiddleware, Ctx>[]
type SetupBlitzOptions<Exports extends object> = {
plugins: BlitzServerPlugin<RequestMiddleware, Ctx, () => Exports>[]
onError?: (err: Error) => void
logger?: ReturnType<typeof BlitzLogger>
}
@@ -130,11 +125,19 @@ const prefetchQueryFactory = (
}
}
export const setupBlitzServer = ({plugins, onError, logger}: SetupBlitzOptions) => {
export const setupBlitzServer = <TPlugins extends SetupBlitzOptions<object>>({
plugins,
onError,
logger,
}: TPlugins) => {
initializeLogger(logger ?? BlitzLogger())
const middlewares = plugins.flatMap((p) => p.requestMiddlewares)
const contextMiddleware = plugins.flatMap((p) => p.contextMiddleware).filter(Boolean)
const pluginExports = plugins.reduce(
(acc, plugin) => ({...plugin.exports!(), ...acc}),
{} as ServerPluginsExports<TPlugins["plugins"]>,
)
const gSSP =
<
@@ -216,7 +219,7 @@ export const setupBlitzServer = ({plugins, onError, logger}: SetupBlitzOptions)
}
}
return {gSSP, gSP, api}
return {gSSP, gSP, api, ...pluginExports}
}
export interface BlitzConfig extends NextConfig {

View File

@@ -0,0 +1,25 @@
const fs = require("fs")
const filesToModify = [
"dist\\index-browser.cjs",
"dist\\index-browser.mjs",
"dist\\chunks\\index-browser.cjs",
"dist\\chunks\\index-browser.mjs",
]
const addDirectives = (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf8")
const newFileContent = "'use client';\n" + fileContent
fs.writeFileSync(filePath, newFileContent)
}
const serverFiles = ["dist\\index-server.cjs", "dist\\index-server.mjs"]
const fixNextRouter = (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf8")
const newFileContent = fileContent.replace("require('next/router');", "")
fs.writeFileSync(filePath, newFileContent)
}
filesToModify.forEach(addDirectives)
serverFiles.forEach(fixNextRouter)

View File

@@ -1,10 +1,8 @@
{
"TODO": "REMOVE PRIVATE FIELD",
"name": "template",
"private": true,
"version": "0.0.0",
"name": "@blitzjs/react-query",
"version": "2.0.0-beta.22",
"scripts": {
"build": "unbuild",
"build": "unbuild && node ./utils/add-directives.js",
"dev": "watch unbuild src --wait=0.2",
"lint": "eslint . --fix",
"test": "vitest run",
@@ -21,8 +19,13 @@
"dist/**"
],
"dependencies": {
"@tanstack/react-query": "4.0.10",
"@typescript-eslint/eslint-plugin": "5.42.1",
"@typescript-eslint/parser": "5.9.1"
"@typescript-eslint/parser": "5.9.1",
"blitz": "workspace:2.0.0-beta.22",
"debug": "4.3.3",
"next": "13.1.6",
"superjson": "1.11.0"
},
"devDependencies": {
"@blitzjs/config": "2.0.0-beta.22",

View File

@@ -0,0 +1,23 @@
export {useQuery, usePaginatedQuery, useInfiniteQuery, useMutation} from "./react-query"
export type {MutateFunction} from "./react-query"
export {
getQueryKey,
getInfiniteQueryKey,
invalidateQuery,
setQueryData,
getQueryClient,
getQueryData,
getQueryKeyFromUrlAndParams,
} from "./react-query-utils"
export {
useQueryErrorResetBoundary,
QueryClientProvider,
QueryClient,
dehydrate,
Hydrate,
} from "@tanstack/react-query"
export type {DefaultOptions, HydrateOptions} from "@tanstack/react-query"

View File

@@ -2,7 +2,7 @@ import {describe, expect, it} from "vitest"
import superJson from "superjson"
import {getQueryKey, getQueryKeyFromUrlAndParams} from "./react-query-utils"
import {RpcClient} from "./rpc"
import {RpcClient} from "blitz"
const API_ENDPOINT = "http://localhost:3000"

View File

@@ -1,7 +1,6 @@
import {QueryClient} from "@tanstack/react-query"
import {serialize} from "superjson"
import {isClient, isServer, AsyncFunc} from "blitz"
import {ResolverType, RpcClient} from "./rpc"
import {isClient, isServer, AsyncFunc, ResolverType, RpcClient} from "blitz"
export type Resolver<TInput, TResult> = (input: TInput, ctx?: any) => Promise<TResult>

View File

@@ -47,6 +47,7 @@ export function useQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp?: boolean,
options?: UseQueryOptions<TResult, TError, TSelectedData> & QueryNonLazyOptions,
): [TSelectedData, RestQueryResult<TSelectedData, TError>]
export function useQuery<
@@ -57,6 +58,7 @@ export function useQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp: boolean,
options: UseQueryOptions<TResult, TError, TSelectedData> & QueryLazyOptions,
): [TSelectedData | undefined, RestQueryResult<TSelectedData, TError>]
export function useQuery<
@@ -67,6 +69,7 @@ export function useQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp = globalThis.__BLITZ_RSC,
options: UseQueryOptions<TResult, TError, TSelectedData> = {},
) {
if (typeof queryFn === "undefined") {
@@ -75,8 +78,12 @@ export function useQuery<
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
let routerIsReady = false
if (isApp) {
routerIsReady = true
} else {
routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
}
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, params)
@@ -129,6 +136,7 @@ export function usePaginatedQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp?: boolean,
options?: UseQueryOptions<TResult, TError, TSelectedData> & QueryNonLazyOptions,
): [TSelectedData, RestPaginatedResult<TSelectedData, TError>]
export function usePaginatedQuery<
@@ -139,6 +147,7 @@ export function usePaginatedQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp: boolean,
options: UseQueryOptions<TResult, TError, TSelectedData> & QueryLazyOptions,
): [TSelectedData | undefined, RestPaginatedResult<TSelectedData, TError>]
export function usePaginatedQuery<
@@ -149,6 +158,7 @@ export function usePaginatedQuery<
>(
queryFn: T,
params: FirstParam<T>,
isApp = globalThis.__BLITZ_RSC,
options: UseQueryOptions<TResult, TError, TSelectedData> = {},
) {
if (typeof queryFn === "undefined") {
@@ -157,8 +167,12 @@ export function usePaginatedQuery<
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
let routerIsReady = false
if (isApp) {
routerIsReady = true
} else {
routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
}
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getQueryKey(queryFn, params)
@@ -222,6 +236,7 @@ export function useInfiniteQuery<
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData> & QueryNonLazyOptions,
isApp?: boolean,
): [TSelectedData[], RestInfiniteResult<TSelectedData, TError>]
export function useInfiniteQuery<
T extends AsyncFunc,
@@ -232,6 +247,7 @@ export function useInfiniteQuery<
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData> & QueryLazyOptions,
isApp: boolean,
): [TSelectedData[] | undefined, RestInfiniteResult<TSelectedData, TError>]
export function useInfiniteQuery<
T extends AsyncFunc,
@@ -242,6 +258,7 @@ export function useInfiniteQuery<
queryFn: T,
getQueryParams: (pageParam: any) => FirstParam<T>,
options: InfiniteQueryConfig<TResult, TError, TSelectedData>,
isApp = globalThis.__BLITZ_RSC,
) {
if (typeof queryFn === "undefined") {
throw new Error("useInfiniteQuery is missing the first argument - it must be a query function")
@@ -249,8 +266,12 @@ export function useInfiniteQuery<
const suspenseEnabled = Boolean(globalThis.__BLITZ_SUSPENSE_ENABLED)
let enabled = isServer && suspenseEnabled ? false : options?.enabled ?? options?.enabled !== null
const suspense = enabled === false ? false : options?.suspense
const routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
let routerIsReady = false
if (isApp) {
routerIsReady = true
} else {
routerIsReady = useRouter().isReady || (isServer && suspenseEnabled)
}
const enhancedResolverRpcClient = sanitizeQuery(queryFn)
const queryKey = getInfiniteQueryKey(queryFn, getQueryParams)

View File

@@ -0,0 +1,95 @@
import "./global"
import {createClientPlugin} from "blitz"
import {DefaultOptions, QueryClient, QueryClientProvider} from "@tanstack/react-query"
import React from "react"
export * from "./client"
interface BlitzReactQueryOptions {
reactQueryOptions?: DefaultOptions
}
export type BlitzRscProviderProps = {
children: JSX.Element
client?: QueryClient
contextSharing?: boolean
}
export const BlitzReactQueryPlugin = createClientPlugin<
BlitzReactQueryOptions,
{queryClient: QueryClient; BlitzRscProvider: React.FC<BlitzRscProviderProps>}
>((options?: BlitzReactQueryOptions) => {
const initializeQueryClient = () => {
const {reactQueryOptions} = options || {}
let suspenseEnabled = reactQueryOptions?.queries?.suspense ?? true
if (!process.env.CLI_COMMAND_CONSOLE && !process.env.CLI_COMMAND_DB) {
globalThis.__BLITZ_SUSPENSE_ENABLED = suspenseEnabled
}
return new QueryClient({
defaultOptions: {
...reactQueryOptions,
queries: {
...(typeof window === "undefined" && {cacheTime: 0}),
retry: (failureCount: number, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
},
...reactQueryOptions?.queries,
suspense: suspenseEnabled,
},
},
})
}
const queryClient = initializeQueryClient()
function resetQueryClient() {
setTimeout(async () => {
// Do these in the next tick to prevent various bugs like https://github.com/blitz-js/blitz/issues/2207
const debug = (await import("debug")).default("blitz:rpc")
debug("Invalidating react-query cache...")
await queryClient.cancelQueries()
await queryClient.resetQueries()
queryClient.getMutationCache().clear()
// We have a 100ms delay here to prevent unnecessary stale queries from running
// This prevents the case where you logout on a page with
// Page.authenticate = {redirectTo: '/login'}
// Without this delay, queries that require authentication on the original page
// will still run (but fail because you are now logged out)
// Ref: https://github.com/blitz-js/blitz/issues/1935
}, 100)
}
const BlitzRscProvider = ({
client = queryClient,
contextSharing = false,
children,
}: BlitzRscProviderProps) => {
if (client) {
return (
<QueryClientProvider
client={client || globalThis.queryClient}
contextSharing={contextSharing}
>
{children}
</QueryClientProvider>
)
}
return <>{children}</>
}
globalThis.queryClient = queryClient
return {
events: {
onSessionCreated: async () => {
resetQueryClient()
},
},
middleware: {},
exports: () => ({
BlitzRscProvider,
queryClient,
}),
}
})

View File

@@ -1,3 +1 @@
export * from "./index-browser"
export const todoServer = true

View File

@@ -0,0 +1,16 @@
const fs = require("fs")
const filesToModify = [
"dist\\index-browser.cjs",
"dist\\index-browser.mjs",
"dist\\index-server.cjs",
"dist\\index-server.mjs",
]
const addDirectives = (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf8")
const newFileContent = "'use client';\n" + fileContent
fs.writeFileSync(filePath, newFileContent)
}
filesToModify.forEach(addDirectives)

View File

@@ -14,6 +14,7 @@ const config: BuildConfig = {
"index-server.cjs",
"index-server.mjs",
"@blitzjs/auth",
"@blitzjs/react-query",
"zod",
],
declaration: true,

View File

@@ -21,7 +21,6 @@
],
"dependencies": {
"@swc/core": "1.3.7",
"@tanstack/react-query": "4.0.10",
"b64-lite": "1.4.0",
"bad-behavior": "1.0.1",
"chalk": "^4.1.0",
@@ -31,12 +30,19 @@
},
"peerDependencies": {
"blitz": "2.0.0-beta.22",
"@blitzjs/react-query": "2.0.0-beta.22",
"next": "*",
"react": "*"
},
"peerDependenciesMeta": {
"@blitzjs/react-query": {
"optional": true
}
},
"devDependencies": {
"@blitzjs/auth": "2.0.0-beta.22",
"@blitzjs/config": "workspace:2.0.0-beta.22",
"@blitzjs/react-query": "workspace:2.0.0-beta.22",
"@types/debug": "4.1.7",
"@types/react": "18.0.25",
"@types/react-dom": "17.0.14",

View File

@@ -1,19 +1,3 @@
export * from "./rpc"
export {useQuery, usePaginatedQuery, useInfiniteQuery, useMutation} from "./react-query"
export type {MutateFunction} from "./react-query"
export {
getQueryKey,
getInfiniteQueryKey,
invalidateQuery,
setQueryData,
getQueryClient,
getQueryData,
} from "./react-query-utils"
export {
useQueryErrorResetBoundary,
QueryClientProvider,
QueryClient,
dehydrate,
} from "@tanstack/react-query"
export {invoke} from "./invoke"
export {invokeWithCtx} from "./invokeWithCtx"

View File

@@ -1,5 +1,4 @@
import {FirstParam, PromiseReturnType, isClient} from "blitz"
import {RpcClient} from "./rpc"
import {FirstParam, PromiseReturnType, isClient, RpcClient} from "blitz"
export function invoke<T extends (...args: any) => any, TInput = FirstParam<T>>(
queryFn: T,

View File

@@ -3,14 +3,12 @@ import {addBasePath} from "next/dist/client/add-base-path"
import {deserialize, serialize, stringify} from "superjson"
import {SuperJSONResult} from "superjson/dist/types"
import {isServer} from "blitz"
import {getQueryKeyFromUrlAndParams, getQueryClient} from "./react-query-utils"
import {ResolverType, RpcClient, RpcClientBase} from "blitz"
export function normalizeApiRoute(path: string): string {
return normalizePathTrailingSlash(addBasePath(path))
}
export type ResolverType = "query" | "mutation"
export interface BuildRpcClientParams {
resolverName: string
resolverType: ResolverType
@@ -18,27 +16,6 @@ export interface BuildRpcClientParams {
httpMethod: string
}
export interface RpcOptions {
fromQueryHook?: boolean
fromInvoke?: boolean
alreadySerialized?: boolean
}
export interface EnhancedRpc {
_isRpcClient: true
_resolverType: ResolverType
_resolverName: string
_routePath: string
}
export interface RpcClientBase<Input = unknown, Result = unknown> {
(params: Input, opts?: RpcOptions, signal?: AbortSignal): Promise<Result>
}
export interface RpcClient<Input = unknown, Result = unknown>
extends EnhancedRpc,
RpcClientBase<Input, Result> {}
// export interface RpcResolver<Input = unknown, Result = unknown> extends EnhancedRpc {
// (params: Input, ctx?: Ctx): Promise<Result>
// }
@@ -144,8 +121,13 @@ export function __internal_buildRpcClient({
meta: payload.meta?.result,
})
if (!opts.fromQueryHook) {
const queryKey = getQueryKeyFromUrlAndParams(routePath, params)
getQueryClient().setQueryData(queryKey, data)
try {
const {getQueryKeyFromUrlAndParams, getQueryClient} = await import(
"@blitzjs/react-query"
)
const queryKey = getQueryKeyFromUrlAndParams(routePath, params)
getQueryClient().setQueryData(queryKey, data)
} catch (e) {}
}
return data
}

View File

@@ -1,67 +1 @@
import "./global"
import {createClientPlugin} from "blitz"
import {DefaultOptions, QueryClient} from "@tanstack/react-query"
export * from "./data-client/index"
interface BlitzRpcOptions {
reactQueryOptions?: DefaultOptions
}
export const BlitzRpcPlugin = createClientPlugin<BlitzRpcOptions, {queryClient: QueryClient}>(
(options?: BlitzRpcOptions) => {
const initializeQueryClient = () => {
const {reactQueryOptions} = options || {}
let suspenseEnabled = reactQueryOptions?.queries?.suspense ?? true
if (!process.env.CLI_COMMAND_CONSOLE && !process.env.CLI_COMMAND_DB) {
globalThis.__BLITZ_SUSPENSE_ENABLED = suspenseEnabled
}
return new QueryClient({
defaultOptions: {
...reactQueryOptions,
queries: {
...(typeof window === "undefined" && {cacheTime: 0}),
retry: (failureCount: number, error: any) => {
if (process.env.NODE_ENV !== "production") return false
// Retry (max. 3 times) only if network error detected
if (error.message === "Network request failed" && failureCount <= 3) return true
return false
},
...reactQueryOptions?.queries,
suspense: suspenseEnabled,
},
},
})
}
const queryClient = initializeQueryClient()
function resetQueryClient() {
setTimeout(async () => {
// Do these in the next tick to prevent various bugs like https://github.com/blitz-js/blitz/issues/2207
const debug = (await import("debug")).default("blitz:rpc")
debug("Invalidating react-query cache...")
await queryClient.cancelQueries()
await queryClient.resetQueries()
queryClient.getMutationCache().clear()
// We have a 100ms delay here to prevent unnecessary stale queries from running
// This prevents the case where you logout on a page with
// Page.authenticate = {redirectTo: '/login'}
// Without this delay, queries that require authentication on the original page
// will still run (but fail because you are now logged out)
// Ref: https://github.com/blitz-js/blitz/issues/1935
}, 100)
}
globalThis.queryClient = queryClient
return {
events: {
onSessionCreated: async () => {
resetQueryClient()
},
},
middleware: {},
exports: () => ({
queryClient,
}),
}
},
)

View File

@@ -3,6 +3,6 @@
"compilerOptions": {
"lib": ["DOM", "ES2015"]
},
"include": ["."],
"include": [".", "../query/src/client/react-query.tsx"],
"exclude": ["dist", "build", "node_modules"]
}

View File

@@ -0,0 +1,18 @@
const fs = require("fs")
const filesToModify = [
"dist\\index-browser.cjs",
"dist\\index-browser.mjs",
"dist\\index-server.cjs",
"dist\\index-server.mjs",
"dist\\chunks\\rpc.cjs",
"dist\\chunks\\rpc.mjs",
]
const addDirectives = (filePath) => {
const fileContent = fs.readFileSync(filePath, "utf8")
const newFileContent = "'use client';\n" + fileContent
fs.writeFileSync(filePath, newFileContent)
}
filesToModify.forEach(addDirectives)

View File

@@ -5,4 +5,5 @@ declare global {
beforeHttpRequest: BeforeHttpRequest
beforeHttpResponse: BeforeHttpResponse
}
var __BLITZ_RSC: boolean
}

View File

@@ -84,4 +84,4 @@ export * from "./utils"
export * from "./types"
export * from "./utils/enhance-prisma"
export * from "./utils/zod"
export {reduceBlitzPlugins} from "./plugin"
export {reduceBlitzClientPlugins} from "./plugin"

View File

@@ -9,6 +9,7 @@ export * from "./utils/enhance-prisma"
export * from "./middleware"
export * from "./paginate"
export * from "./logging"
export type {ServerPluginsExports, ClientPluginsExports} from "./plugin"
export {startWatcher, stopWatcher} from "./cli/utils/routes-manifest"
@@ -36,7 +37,7 @@ export type BlitzServerPlugin<
> = {
requestMiddlewares: RequestMiddlewareType[]
contextMiddleware?: (ctx: TCtx) => TCtx
exports?: TExports
exports?: () => TExports
}
export function createServerPlugin<

View File

@@ -4,6 +4,7 @@ import type {
Simplify,
UnionToIntersection,
} from "./index-browser"
import {BlitzServerPlugin} from "./index-server"
import {isClient} from "./utils"
export function merge<T, U>([...fns]: Array<(args: T) => U>) {
@@ -19,7 +20,7 @@ const compose =
(x: React.ComponentType<any>) =>
rest.reduceRight((y, f) => f(y), x)
export type PluginsExports<TPlugins extends readonly ClientPlugin<object>[]> = Simplify<
export type ClientPluginsExports<TPlugins extends readonly ClientPlugin<object>[]> = Simplify<
UnionToIntersection<
{
[I in keyof TPlugins & number]: ReturnType<TPlugins[I]["exports"]>
@@ -27,7 +28,15 @@ export type PluginsExports<TPlugins extends readonly ClientPlugin<object>[]> = S
>
>
export function reduceBlitzPlugins<TPlugins extends readonly ClientPlugin<object>[]>({
export type ServerPluginsExports<TPlugins extends readonly BlitzServerPlugin<object>[]> = Simplify<
UnionToIntersection<
{
[I in keyof TPlugins & number]: TPlugins[I]["exports"] extends () => infer R ? R : never
}[number]
>
>
export function reduceBlitzClientPlugins<TPlugins extends readonly ClientPlugin<object>[]>({
plugins,
}: {
plugins: TPlugins
@@ -72,7 +81,7 @@ export function reduceBlitzPlugins<TPlugins extends readonly ClientPlugin<object
onRpcError: merge<Error, Promise<void>>([]),
onSessionCreated: merge<void, Promise<void>>([]),
},
exports: {} as PluginsExports<TPlugins>,
exports: {} as ClientPluginsExports<TPlugins>,
providers: [] as BlitzProviderComponentType[],
},
)

View File

@@ -2,6 +2,29 @@ import {UrlObject} from "url"
// Context for plugins to declaration merge stuff into
export interface Ctx {}
export type ResolverType = "query" | "mutation"
export interface EnhancedRpc {
_isRpcClient: true
_resolverType: ResolverType
_resolverName: string
_routePath: string
}
export interface RpcOptions {
fromQueryHook?: boolean
fromInvoke?: boolean
alreadySerialized?: boolean
}
export interface RpcClientBase<Input = unknown, Result = unknown> {
(params: Input, opts?: RpcOptions, signal?: AbortSignal): Promise<Result>
}
export interface RpcClient<Input = unknown, Result = unknown>
extends EnhancedRpc,
RpcClientBase<Input, Result> {}
export interface RouteUrlObject extends Pick<UrlObject, "pathname" | "query" | "href"> {
pathname: string
href: string

View File

@@ -1,6 +1,6 @@
import {describe, it, expect, vi} from "vitest"
import {ClientPlugin} from "../src/index-server"
import {reduceBlitzPlugins, merge, pipe} from "../src/plugin"
import {reduceBlitzClientPlugins, merge, pipe} from "../src/plugin"
vi.spyOn(console, "log")
@@ -23,7 +23,7 @@ describe("pipe", () => {
})
})
describe("reduceBlitzPlugins", () => {
describe("reduceBlitzClientPlugins", () => {
it("should reduce plugins", async () => {
const plugin1: ClientPlugin<{
foo: number
@@ -89,7 +89,7 @@ describe("reduceBlitzPlugins", () => {
bar: 3,
}),
}
const {middleware, events, exports} = reduceBlitzPlugins({plugins: [plugin1, plugin2]})
const {middleware, events, exports} = reduceBlitzClientPlugins({plugins: [plugin1, plugin2]})
expect(middleware.beforeHttpRequest).toBeDefined()
expect(middleware.beforeHttpResponse).toBeDefined()
expect(events.onRpcError).toBeDefined()

View File

@@ -1 +0,0 @@
export const todo = true

1156
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff