1
0
mirror of synced 2026-02-06 09:00:12 -05:00

Compare commits

..

36 Commits

Author SHA1 Message Date
Brandon Bayer
b1ef45bf28 2.0.0-alpha.2 - fix generator npm package dist 2022-04-14 20:44:45 -04:00
Brandon Bayer
c6e8df5efd release 2.0.0-alpha.1 2022-04-14 20:31:40 -04:00
Brandon Bayer
46a34c7b3a install changeset 2022-04-14 20:28:16 -04:00
Dillon Raphael
18d4ef74a9 Migrate cli generate command (#3294)
* migrate generate command

* tidy up the help command
2022-04-14 16:49:59 -04:00
Dillon Raphael
212a1cb941 Cli codegen command (#3276)
* WIP: init cli + init env utils

* remove pnpm global link artifacts

* WIP: migrate legacy new cli command

* change enums to objects

* remove chromedriver package from integration test

* create cli bin file that requires blitz/dist/index.cj

* fix prisma-utils pEvent

* fix missing modules

* use form key types & rename bin script

* init cli-codegen

* add fs-extra package to blitz-next

* read config file as a buffer then eval the string

Co-authored-by: Dillon Raphael <dillonraphael@Dillons-MBP.hitronhub.home>
2022-04-14 11:17:19 -04:00
Aleksandra
151dcc308e Add RpcClientPlugin 🎉 (#3302) 2022-04-13 14:24:26 +02:00
Aleksandra
fd90cbedb4 Support blitz next CMD (#3272) 2022-04-13 11:23:49 +02:00
Brandon Bayer
10d27c74af refactor toolkit setup a bit for better DX (#3297) 2022-04-12 15:56:15 -04:00
Brandon Bayer
9810d984f1 toolkit: remove hook exports from setupClient (#3287)
* cleanup exports

* bump

* Update example app

Co-authored-by: Aleksandra <alexsandra.sikora@gmail.com>
2022-04-12 14:31:10 +02:00
Brandon Bayer
10574b7359 Add RPC package to toolkit, including magical imports 🎉 (#3278)
* wip

* webpack loader working

* time to switch to babel

* wip

* wip - next broken

* build & babel working, but resolver not loaded at runtime

* webpack loaders fully working!

* rpc server handler ported

* fully working e2e!

* ci

* fix

* fix

* fix test

* refactor integration test setup

* port rpc integration test

* remove duplicate constants

* fix timeout

* add more explicit timeouts for tests

* use pnpm exec

* add custom _document for auth test

* await killapp

* add next as dependency for test utils

* update pnpm ci

* add vitest config file

* add hookTimeout to global vitest file

Co-authored-by: Dillon Raphael <dillon@creatorsneverdie.com>
2022-04-11 14:18:02 -04:00
Aleksandra
c28684b8e5 Add jest config to blitz-next package (#3267) 2022-04-08 13:38:09 +02:00
Dillon Raphael
a590820c14 Cli (#3270)
* WIP: init cli + init env utils

* remove pnpm global link artifacts

* WIP: migrate legacy new cli command

* change enums to objects

* remove chromedriver package from integration test

* create cli bin file that requires blitz/dist/index.cj

* fix prisma-utils pEvent

* fix missing modules

* use form key types & rename bin script

Co-authored-by: Dillon Raphael <dillonraphael@Dillons-MBP.hitronhub.home>
2022-04-07 18:02:52 -04:00
Dillon Raphael
ff3ef58a47 Auth tests (#3260)
* Auth tests setup

* WIP: Updated test-utils

* WIP: Fix lowdb for tests + passing unauth tests + create elementByXpath webdriver chain

* Using prisma & sqlite for tests

* manypkg fix dependency

* fix server mode for auth test

* Add .env template to auth integration test

* Add default value for .env + fix how chrome window is rendered in next-webdriver + install chromedriver in package.json

* Rename test directory

* Update pnpm-lock

* trying to set debug port

* add chrome to ci

* Install chrome with apt-get for github actions

* set headless and chrome bin in env file

* find chrome path inside webdriver

* run the right chrome command in github action based on os

* fix step cannot have both the  and  keys

* add explicit bash shell for github action

* use matrix os expression and run if statement under chrome run

* dont cancel in progress tests to see if one works

* add chrome driver

* remove potential conflicting chromedriver versions

* let selenium find chromedriver path instead of manually setting it

* set chrome option to no gpu & disable shm usage

* remove chrome install github action

* use chromedriver path for binary

* add --headless attribute to chrome driver options

* use chrome bin instead of chromedriver bin

* set path if chromewebdriver env variable present

* set next commands inside try/catch

* remove try/catch

* increase timeout

* disable gpu

* add back headless env

* set shell to bash

* set default shell

* set timeout for webdriver test function

* remove run chromedriver for windows since we're using bash

* scl_source enable

* remove scl_source enable

* add try catch

* remove bash default

* whitelist ips for chromedriver in windows matrix

* set shell to cmd for windows matrix

* console.log path

* force cmd for running chromedriver

* start chromedriver for cmd

* set default windows command to pwsh

* set path for windows test command

* set dynamic path for windows os

* remove if statement for test command

* remove chromedriver from github yaml

* remove windows os from tests

Co-authored-by: Aleksandra <alexsandra.sikora@gmail.com>
Co-authored-by: Dillon Raphael <dillonraphael@Dillons-MacBook-Pro.local>
Co-authored-by: Dillon Raphael <dillonraphael@Dillons-MBP.hitronhub.home>
2022-04-07 12:46:37 -04:00
Aleksandra
e33c81a70f Add useParams to blitz-next (#3269) 2022-04-05 18:34:16 +02:00
Aleksandra
dc41f8eedc Add paginate utility to blitz package (#3266) 2022-04-04 11:04:43 +02:00
Brandon Bayer
f49aa37892 toolkit: fix some various package and build issues (#3263) 2022-04-02 16:06:25 -04:00
Aleksandra
e218d5f84d Migrate generator package (#3245) 2022-03-30 14:34:49 +02:00
Ante Primorac
2eb4f791a8 Add typings for PrismaStorage (#3243) 2022-03-28 12:48:55 +02:00
Aleksandra
fa0850d0a3 Migrate middleware to core package (#3238) 2022-03-24 16:14:00 +01:00
Aleksandra
2d3c6cbfb8 Add zod utilities to blitz core (#3233) 2022-03-11 14:50:10 -05:00
Aleksandra
6ff7e99d15 Include client error patch in blitz core (#3232) 2022-03-11 14:35:11 -05:00
Aleksandra
77ca03af96 Add enhancePrisma to blitz core (#3231) 2022-03-11 14:15:34 -05:00
Aleksandra
f56ef9b2c5 Add connectMiddleware to blitz-next (#3230) 2022-03-11 13:18:17 -05:00
Aleksandra
991b4a9db0 Export ErrorBoundary from blitz-next (#3229) 2022-03-11 10:10:23 -05:00
Aleksandra
0c8edbb8b2 Move errors.ts to blitz package (#3228) 2022-03-10 11:55:39 -05:00
Aleksandra
2d75a2b7c7 Move runMiddlewares to blitz package (#3227) 2022-03-10 11:38:01 -05:00
Aleksandra
b878c6845e Figure out relations between Blitz packages (#3195) 2022-03-10 10:33:20 -05:00
Brandon Bayer
cb0da6a0cb upgrade some toolkit deps (#3194) 2022-03-09 12:12:29 -05:00
Aleksandra
a47c643145 Add client side support for auth plugin (#3161) 2022-02-21 15:53:02 +01:00
Aleksandra
945c66a53c Properly add sessionConfig to globalThis type (#3174) 2022-02-18 15:09:31 +01:00
Aleksandra
50421a19ed Add default values to public-hoist-pattern (#3175) 2022-02-18 15:07:20 +01:00
Aleksandra Sikora
926e5cf10f Add auth client unit tests (#3160) 2022-02-09 13:19:37 +01:00
Aleksandra Sikora
6228366ea3 Migrate auth code to Blitz toolkit, add server & client setup (#3147) 2022-02-09 12:59:13 +01:00
Brandon Bayer
bcb56eb79d cleanup packages 2022-01-16 17:53:15 -05:00
Brandon Bayer
80f0c81130 Set up fancy new monorepo for blitz toolkit with pnpm, turborepo, unbuild, vitest (#3129) 2022-01-16 17:45:02 -05:00
Brandon Bayer
0847896968 mit license 2022-01-16 17:26:30 -05:00
5059 changed files with 23596 additions and 536703 deletions

8
.changeset/README.md Normal file
View File

@@ -0,0 +1,8 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)
We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)

11
.changeset/config.json Normal file
View File

@@ -0,0 +1,11 @@
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [["blitz"], ["@blitzjs/*"]],
"linked": [],
"access": "restricted",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": ["web", "test-*"]
}

View File

@@ -0,0 +1,9 @@
---
"blitz": patch
"@blitzjs/auth": patch
"@blitzjs/next": patch
"@blitzjs/rpc": patch
"@blitzjs/generator": patch
---
initial publish

View File

@@ -0,0 +1,5 @@
---
"@blitzjs/generator": patch
---
fix generator npm package dist

18
.changeset/pre.json Normal file
View File

@@ -0,0 +1,18 @@
{
"mode": "pre",
"tag": "alpha",
"initialVersions": {
"web": "0.0.0",
"test-auth": "0.0.0",
"test-rpc": "0.0.0",
"test-utils": "0.0.0",
"blitz": "2.0.0-alpha.0",
"@blitzjs/auth": "2.0.0-alpha.0",
"@blitzjs/next": "2.0.0-alpha.0",
"@blitzjs/rpc": "2.0.0-alpha.0",
"@blitzjs/config": "0.0.0",
"@blitzjs/generator": "2.0.0-alpha.0",
"template": "0.0.0"
},
"changesets": ["ninety-pets-heal", "poor-peas-lick"]
}

View File

@@ -1,55 +1,2 @@
node_modules
reports
*.log
.nyc_output
**/coverage
tsconfig.tsbuildinfo
**/.blitz/**
**/.next/**
**/dist/**
**/.vercel/**
**/.test*
/examples/auth2
prettier.config.*
jest.config.*
jest.setup.*
babel.config.*
eslint.config.*
/__mocks__
/__fixturse
/assets
/patches
/rfc-docs
/scripts
/types
/recipes/*/templates
/packages/generator/templates
/packages/cli/lib
/packages/babel-preset/src/fix-node-file-trace/tests/**
/test/integration/**/out/**
/nextjs/packages/create-next-app
// COPIED FROM nextjs/.eslintignore
/nextjs/**/.next/**
/nextjs/**/_next/**
/nextjs/**/dist/**
/nextjs/examples/**
/nextjs/packages/next/bundles/webpack/packages/*.runtime.js
/nextjs/packages/next/compiled/**/*
/nextjs/packages/react-refresh-utils/**/*.js
/nextjs/packages/react-dev-overlay/lib/**
/nextjs/**/__tmp__/**
/nextjs/.github/actions/next-stats-action/.work
/nextjs/packages/next-codemod/transforms/__testfixtures__/**/*
/nextjs/packages/next-codemod/transforms/__tests__/**/*
/nextjs/packages/next-codemod/**/*.js
/nextjs/packages/next-codemod/**/*.d.ts
/nextjs/packages/next-env/**/*.d.ts
/nextjs/packages/next/build/swc/tests/fixture/**
/nextjs/test/integration/async-modules/**
/nextjs/test/integration/eslint/**
/nextjs/test/integration/babel/**
/nextjs/test-timings.json
**/package.json
*.d.ts

View File

@@ -1,119 +0,0 @@
module.exports = {
parser: "@babel/eslint-parser",
env: {
browser: true,
commonjs: true,
es6: true,
node: true,
},
parserOptions: {
ecmaVersion: 6,
requireConfigFile: false,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
babelOptions: {
presets: ["@babel/preset-env", "@babel/preset-react"],
caller: {
// Eslint supports top level await when a parser for it is included. We enable the parser by default for Babel.
supportsTopLevelAwait: true,
},
},
},
plugins: ["import", "unicorn", "simple-import-sort"],
extends: ["react-app"],
rules: {
"react/react-in-jsx-scope": "off", // React is always in scope with Blitz
"jsx-a11y/anchor-is-valid": "off", //Doesn't play well with Blitz/Next <Link> usage
"import/first": "off",
"import/no-default-export": "error",
"require-await": "error",
"no-async-promise-executor": "error",
"unicorn/filename-case": [
"error",
{
case: "kebabCase",
},
],
"simple-import-sort/imports": [
"warn",
{
groups: [
[
// Side effect imports.
"^\\u0000",
// Packages.
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
"^@?\\w",
// Absolute imports and other imports such as Vue-style `@/foo`.
// Anything that does not start with a dot.
"^[^.]",
// Relative imports.
// Anything that starts with a dot.
"^\\.",
],
],
},
],
},
overrides: [
{
files: ["**/*.ts", "**/*.tsx"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
project: `./tsconfig.eslint.json`,
},
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-floating-promises": "error",
// note you must disable the base rule as it can report incorrect errors
"no-use-before-define": "off",
// "@typescript-eslint/no-use-before-define": ["error"],
// note you must disable the base rule as it can report incorrect errors
"no-redeclare": "off",
"@typescript-eslint/no-redeclare": ["error"],
},
},
{
files: ["examples/**", "recipes/**"],
rules: {
"import/no-default-export": "off",
"unicorn/filename-case": "off",
"@typescript-eslint/no-floating-promises": "off",
},
},
{
files: ["examples/**"],
plugins: ["cypress"],
parserOptions: {
project: null,
},
env: {
"cypress/globals": true,
},
rules: {
"simple-import-sort/imports": "off",
},
},
{
files: ["packages/cli/src/commands/**/*"],
rules: {
"require-await": "off",
},
},
{
files: ["test/**", "**/__fixtures__/**"],
rules: {
"import/no-default-export": "off",
"require-await": "off",
"unicorn/filename-case": "off",
},
},
],
}

3
.github/CODEOWNERS vendored
View File

@@ -2,7 +2,4 @@
* @flybayer @beerose
# packages/cli/**/* @aem, @flybayer
# packages/generator/**/* @aem @flybayer
packages/generator/templates**/* @flybayer
# packages/installer/**/* @aem @flybayer

View File

@@ -1,18 +0,0 @@
#!/usr/bin/env node
const fs = require("fs")
const yarnOut = fs.readFileSync(0, {encoding: "utf8"})
const [installTimeString] = /(?<=^Done in )\d+\.\d+(?=s\.$)/m.exec(yarnOut)
const installTime = Number(installTimeString)
console.log(`Install time: ${installTime}s`)
if (installTime < 30) {
console.log("We're below 30 secs. That's awesome!")
} else if (installTime < 50) {
console.log("We're below 50 secs. That's fine!")
} else {
console.log("We're above 50 secs. That's not great!")
process.exit(1)
}

View File

@@ -1,26 +0,0 @@
name: Size Check
on:
pull_request:
branches: [master, canary]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Compressed Size
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Count size
uses: preactjs/compressed-size-action@v2
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
exclude: "{./nextjs/**}"

View File

@@ -11,467 +11,28 @@ concurrency:
cancel-in-progress: true
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules
id: yarn-cache
uses: actions/cache@v2
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
**/node_modules
key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.node_version}}-yarn-v14-
- name: Install dependencies
run: yarn install --frozen-lockfile --silent
env:
CI: true
- name: manypkg lint
run: yarn manypkg check
env:
CI: true
- name: Build packages
run: yarn build
env:
CI: true
- name: yarn lint
run: yarn lint
env:
CI: true
build-linux:
name: Build
runs-on: ubuntu-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache node_modules
id: yarn-cache
uses: actions/cache@v2
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
**/node_modules
key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.node_version}}-yarn-v14-
- run: yarn install --frozen-lockfile --check-files
- name: Build Packages
run: yarn build
- uses: actions/cache@v2
id: cache-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
testBlitzPackages:
name: Blitz - Test Packages
needs: build-linux
runs-on: ubuntu-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Setup kernel to increase watchers
if: runner.os == 'Linux'
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- name: Test Blitz Packages
run: yarn testonly:packages
env:
CI: true
testNextPackages:
name: Next - Test Packages
defaults:
run:
working-directory: nextjs
needs: build-linux
runs-on: ubuntu-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- name: Setup kernel to increase watchers
if: runner.os == 'Linux'
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- name: Test Next Packages
run: yarn testonly:packages
env:
CI: true
testBlitzExamples:
timeout-minutes: 30
name: Blitz - Test Example Apps (ubuntu-latest)
needs: build-linux
runs-on: ubuntu-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
DEBUG: blitz:session
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
# Needed to get cypress binary
- run: yarn cypress install
- name: Install sass
run: yarn install -W sass
- name: Setup kernel to increase watchers
if: runner.os == 'Linux'
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- name: Test examples
run: yarn testonly:examples
env:
CI: true
testBlitzExamplesWin:
timeout-minutes: 30
name: Blitz - Test Example Apps (windows-latest)
runs-on: windows-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
# - name: Get yarn cache directory path
# id: yarn-cache-dir-path
# run: echo "::set-output name=dir::$(yarn cache dir)"
# - name: Cache node_modules
# id: yarn-cache
# uses: actions/cache@v2
# with:
# path: |
# ${{ steps.yarn-cache-dir-path.outputs.dir }}
# **/node_modules
# key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-
- run: yarn install --frozen-lockfile --check-files
- name: Build Packages
run: yarn build
# Needed to get cypress binary
- run: yarn cypress install
- name: Install sass
run: yarn install -W sass
- name: Test examples
run: yarn testonly:examples
env:
CI: true
checkPrecompiled:
name: Check Pre-compiled
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- run: ./scripts/check-pre-compiled.sh
testUnit:
name: Nextjs - Test Unit
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- run: node run-tests.js --type unit -g 1/1
testIntegrationBlitz:
name: Blitz - Test Integration
needs: build-linux
runs-on: ubuntu-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
# TODO: remove after we fix watchpack watching too much
- name: Setup kernel to increase watchers
if: runner.os == 'Linux'
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- run: xvfb-run node nextjs/run-tests.js -c 3
testIntegrationBlitzWin:
name: Blitz - Test Integration (Windows)
runs-on: windows-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
# - name: Get yarn cache directory path
# id: yarn-cache-dir-path
# run: echo "::set-output name=dir::$(yarn cache dir)"
# - name: Cache node_modules
# id: yarn-cache
# uses: actions/cache@v2
# with:
# path: |
# ${{ steps.yarn-cache-dir-path.outputs.dir }}
# **/node_modules
# key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-
- run: yarn install --frozen-lockfile --check-files
- name: Build Packages
run: yarn build
- run: node nextjs/run-tests.js
testIntegration:
name: Nextjs - Test Integration
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
build:
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
group: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "14"
# TODO: remove after we fix watchpack watching too much
- name: Setup kernel to increase watchers
if: runner.os == 'Linux'
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- run: xvfb-run node run-tests.js -g ${{ matrix.group }}/20 -c 3
testIntegrationWin:
name: Nextjs - Test Integration (Windows)
runs-on: windows-latest
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
os:
- ubuntu-latest
node_version:
- 16
name: Node ${{ matrix.node_version }} - ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- uses: pnpm/action-setup@646cdf48217256a3d0b80361c5a50727664284f2
with:
fetch-depth: 25
- name: Use Node.js
version: 6.32.6
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: "14"
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
# - name: Get yarn cache directory path
# id: yarn-cache-dir-path
# run: echo "::set-output name=dir::$(yarn cache dir)"
# - name: Cache node_modules
# id: yarn-cache
# uses: actions/cache@v2
# with:
# path: |
# ${{ steps.yarn-cache-dir-path.outputs.dir }}
# **/node_modules
# key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-${{ hashFiles('yarn.lock') }}
# restore-keys: |
# ${{ runner.os }}-${{ runner.node_version}}-yarn-v14-
- run: yarn install --frozen-lockfile --check-files
- name: Build Packages
run: yarn build
- run: node run-tests.js test/integration/basic/test/index.test.js test/integration/production/test/index.test.js test/integration/multi-pages/test/index.test.js
working-directory: nextjs
testElectron:
name: Nextjs - Test Electron
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
BLITZ_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
TEST_ELECTRON: 1
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
# TODO: remove after we fix watchpack watching too much
- name: Setup kernel to increase watchers
run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
- run: cd test/integration/with-electron/app && yarn
- run: xvfb-run node run-tests.js test/integration/with-electron/test/index.test.js
testsPass:
name: thank you, next
runs-on: ubuntu-latest
needs:
[
checkPrecompiled,
testIntegration,
testIntegrationBlitz,
testIntegrationBlitzWin,
testUnit,
testBlitzPackages,
testNextPackages,
testBlitzExamples,
testBlitzExamplesWin,
]
steps:
- run: exit 0
testFirefox:
name: Nextjs - Test Firefox (production)
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
HEADLESS: true
BROWSER_NAME: "firefox"
BLITZ_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- run: node run-tests.js -c 1 test/integration/production/test/index.test.js
testSafari:
name: Nextjs - Test Safari (production)
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: build-linux
env:
BROWSERSTACK: true
BROWSER_NAME: "safari"
BLITZ_TELEMETRY_DISABLED: 1
SKIP_LOCAL_SELENIUM_SERVER: true
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js -c 1 test/integration/production/test/index.test.js'
testSafariOld:
name: Nextjs - Test Safari 10.1 (nav)
defaults:
run:
working-directory: nextjs
runs-on: ubuntu-latest
needs: [build-linux, testSafari]
env:
BROWSERSTACK: true
LEGACY_SAFARI: true
BROWSERNAME: "safari"
BLITZ_TELEMETRY_DISABLED: 1
SKIP_LOCAL_SELENIUM_SERVER: true
BROWSERSTACK_USERNAME: ${{ secrets.BROWSERSTACK_USERNAME }}
BROWSERSTACK_ACCESS_KEY: ${{ secrets.BROWSERSTACK_ACCESS_KEY }}
steps:
- uses: actions/cache@v2
id: restore-build
with:
path: ./*
key: ${{ runner.os }}-${{ github.sha }}
- run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js test/integration/production-nav/test/index.test.js'
node-version: ${{ matrix.node_version }}
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm manypkg check
- run: pnpm build
- run: pnpm lint
- run: pnpm build:apps
- run: pnpm test

34
.gitignore vendored
View File

@@ -1,3 +1,37 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
node_modules
.pnp
.pnp.js
# testing
coverage
.next/
out/
build
dist
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env.local
.env.development.local
.env.test.local
.env.production.local
# turbo
.turbo
.log
.DS_Store
.idea

View File

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

View File

@@ -1,5 +0,0 @@
# .kodiak.toml
# Minimal config. version is the only required field.
version = 1
merge.automerge_label = "0 - <(^_^)> - merge it! ✌️"
approve.auto_approve_usernames = ["flybayer", "beerose", "depfu"]

View File

@@ -1 +1 @@
14.18.1
16.13.2

View File

@@ -1,16 +0,0 @@
.DS_Store
.prettierrc
.nyc_output
.travis.yml
coverage
coverage.lcov
bench
docs
src
examples
babel.config.js
test
CONTRIBUTING.md
CODE_OF_CONDUCT.md
*.ts
!*.d.ts

6
.npmrc
View File

@@ -1 +1,7 @@
save-exact=true
public-hoist-pattern[]=secure-password
public-hoist-pattern[]=*types*
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=@prettier/plugin-*
public-hoist-pattern[]=*prettier-plugin-*

View File

@@ -16,10 +16,5 @@ reports
tsconfig.tsbuildinfo
dist
bin
!packages/blitz/src/bin
packages/generator/templates/**
.github/ISSUE_TEMPLATE/bug_report.md
nextjs/packages/next/compiled/**
// Because file from nextjs upstream isn't formatted properly
nextjs/packages/next/build/webpack-config.ts
packages/generator/templates/**

View File

@@ -2,90 +2,18 @@
[Read the Contributing Guide at Blitzjs.com](https://blitzjs.com/docs/contributing)
## Notes For Core Team
## To run tests
### To Publish a new NPM Package under `@blitzjs/` namespace
Make sure you have `chromedriver` installed for your Chrome version. You can install it with
1. cd into the package directory
2. Run `npm publish --tag danger --access public`
- `--access public` is required because scoped packages are set to private by default
- `brew install --cask chromedriver` on Mac OS X
- `chocolatey install chromedriver` on Windows
- Or manually download the version that matches your installed chrome version (if there's no match, download a version under it, but not above) from the [chromedriver repo](https://chromedriver.storage.googleapis.com/index.html) and add the binary to `<next-repo>/node_modules/.bin`
### Syncing Next.js Fork
You may also have to [install Rust](https://www.rust-lang.org/tools/install) and build our native packages to see all tests pass locally. We check in binaries for the most common targets and those required for CI so that most people don't have to, but if you do not see a binary for your target in `packages/next/native`, you can build it by running `yarn --cwd packages/next build-native`. If you are working on the Rust code and you need to build the binaries for ci, you can manually trigger [the workflow](https://github.com/vercel/next.js/actions/workflows/build_native.yml) to build and commit with the "Run workflow" button.
1. Run `yarn push-nextjs`
- If it fails with an error of `git-subrepo: Can't commit: 'subrepo/nextjs' doesn't contain upstream HEAD:`, then run `yarn push-nextjs --force` (see https://github.com/ingydotnet/git-subrepo/issues/530)
2. Create new git branch for the upgrade
3. In the forked repo (https://github.com/blitz-js/next.js), run:
1. `git pull`
2. `git fetch --all`
3. `git merge v10.2.0` (change the version to be the version you are updating to)
4. Run `rm -rf examples && git add examples`
5. To resolve conflict with their version for a path, like docs, run this:
- `git checkout --theirs docs && git add docs`
6. Resolve all merge conflicts and complete merge
7. Run `yarn` and make sure all builds complete
8. Run `yarn lint` and fix any issues
9. Commit all changes to finish merge
10. `git push`
4. Run `yarn pull-nextjs`
5. Run `yarn`
6. Run `yarn manypkg check` and optionally `yarn manypkg fix` to fix any issues
7. Under `nextjs/`, run `./scripts/check-pre-compiled.sh` and commit the changes
8. Run `yarn build:nextjs`
9. Run `yarn lint` - fix any issues
10. Run `yarn build` - fix any issues
11. Run `yarn test:nextjs-size` and update tests if there are any failures
12. Open PR and fix any failing tests
13. Update any references to nextjs in new code including imports like `next/image`, etc.
14. Any doc updates needed?
15. Merge PR
16. `yarn push-nextjs`
#### Troubleshooting
##### yarn lint - Failed to load parser
Caused by invalid version of `@babel/eslint-parser`. `7.13.14` is a working version. I think it may be an incompatibility between this version and the version of eslint?
- change version of eslint-parser
- run `yarn --check-files`
- run `./scripts/check-pre-compiled.sh` from `./nextjs/`
- run `yarn build:nextjs` from root
- Try linting again
Running all tests:
```
~/c/blitz> yarn lint
yarn run v1.22.10
$ eslint --ext ".js,.ts,.tsx" .
Oops! Something went wrong! :(
ESLint: 7.21.0
Error: Failed to load parser './parser.js' declared in 'examples/auth/.eslintrc.js » eslint-config-blitz » eslint-config-next': Cannot find module '@babel/parser'
at webpackEmptyContext (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:1:33258)
at Object.73139 (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:2194:783181)
at __nccwpck_require__ (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:2194:1065271)
at Object.eslintParser (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/bundle.js:1:43676)
at Object.<anonymous> (/Users/b/c/blitz/nextjs/packages/next/dist/compiled/babel/eslint-parser.js:1:100)
at Module._compile (/Users/b/c/blitz/node_modules/v8-compile-cache/v8-compile-cache.js:192:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:1027:10)
at Module.load (internal/modules/cjs/loader.js:863:32)
at Function.Module._load (internal/modules/cjs/loader.js:708:14)
at Module.require (internal/modules/cjs/loader.js:887:19)
error Command failed with exit code 2.
```
##### Failed to compile - LICENSE
This error occurs sometimes when you import code from packages/next/build/utils.ts into some other code like config-shared.ts. Solution is to move the code into another file.
```
Failed to compile.
../../../packages/next/dist/compiled/webpack/LICENSE
Module parse failed: Unexpected token (1:10)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file. See webpack.js.org/concepts#loaders
> Copyright JS Foundation and other contributors
|
| Permission is hereby granted, free of charge, to any person obtaining
```sh
pnpm test
```

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2021 Brandon Bayer
Copyright (c) 2022 Blitz Revolution Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,124 +0,0 @@
# 2020-08-17 Blitz Contributor Call
- Attending: Brandon Bayer, Adam Markon, Kellen Mace, Myron Davis, Dwight Watson
- Brandon:
- Auth out, set up by default in canary release
- Need to work on a potential CSRF bug
- Next major release will include auth by default and allow you to choose your form library
- Next auth features are email confirmation
- After that, logging and plugins are next
- Adam:
- Overhauled the recipe infrastructure. Now using jscodeshift instead of recast
- Added support for conditional JSX in templates
- Going to work on custom templates next
- Dwight
- Has been opening issues for problems
- Made a few PRs for some issues
# 2020-07-07 Blitz Contributor Call
- Attending: Brandon Bayer, Robert Rosenburg, Jeremy Liberman
- Brandon:
- Finishing up session managment code
- Waiting for review of session managment code from Rishabh
- Will be working on actual auth code (vs session management) like password hashing, password reset, social login, etc.
- Benefits of our session managment vs rails
- Dont have to redirect to login page
- Using top level error component that catches authentication errors
- You can login from anywhere, during sign up
- Robert:
- Waiting on Kirstinas website designs. Desktop design is finished, but she's working on mobile design
- Code snippet / sandbox of blitz code for the website
# 2020-06-23 Blitz Contributor Call
- Attending: Brandon Bayer, Robert Rosenberg, Justin Hall, Adam Markon
- Brandon:
- Server side session management code is mostly set up. Still need to integrate client side of RPC calls to expose session information
- Identity verification/Oauth integrations still need to be firmed up
- Once auth is wrapped up we should be ready to start on plugins
- Adam:
- Been experimenting with smart page generation based on the current schema model
- MDX installer recipes
- Robert:
- Waiting on designs from Kristina
- Prism component from theme ui customization? If not try and grab source code from docusaurus line highlighting component
- Justin:
- Updated the tutorial
- Helped with various bug fixes
# 2020-06-17 Blitz Contributor Call
- Attending: Brandon Bayer, Fran Zekan, Sigurd Wahl
- Brandon:
- Spent the past week implementing HTTP middlware which is now released!
- Now working on implementing session management!
- Fran:
- Finishing up the PR for adding `blitz db seed`
- Sigurd:
- New to the community, slowly learning the codebase
- Exicited to get a first PR in here sometime soon :)
# 2020-06-09 Blitz Contributor Call
- Attending: Brandon Bayer, Rudi Yardley, Fran Zekan, Adam Markon, Robert Rosenberg, Kristina Matuska
- Brandon:
- blitzjs.com published, docs + marketing site v0.1 live
- For now most of the docs copied from react-query and Next, we should eventually clean them up so they&#39;re stylistically similar to ours, but really easy to start
- HTTP middleware is almost done, just fixing a few edge cases
- Authentication is up next after that, translating pseudocode into actual code
- Kristina:
- Homepage design mostly wrapped up right now, have to finish up mobile and light mode and then ready to move on to the rest of the docs
- Syntax highlighting, we can customize colors
- Sidebar inspiration from tailwindcss
- Robert:
- Decided to wait to convert existing components to theme UI until the final design is done
- More docs content:
- More guides
- Improve the tutorial to incorporate relationship generation
- Add branches for canary and master
- If you add a feature you can add your documentation to the canary branch
- Adam:
- blitz generate model finished!
- Installer rewrite complete
- At similar place to what Gatsby has for installing stuff
- Next up:
- Support gatsby MDX recipes
- Make all code generators aware of actual model attributes
- Fran:
- Working on a package for getting the blitz config anywhere - getConfig()
- Prevent app from &quot;warming&quot; the server when deployed as server rather than serverless
- Testing examples - e2e with cypress and unit tests with Jest so we can link to a testing setup in the docs/getting started guide
- Rudi:
- Extracted out the @blitzjs/file-pipeline (previously synchronizer)
- Extracted out the @blitzjs/display package
- Working on various Next.js compatibility issues
- Debugging a bug in blitz start where it gets stuck at \_manifest.json
# 2020-05-26 Blitz Contributor Call
- Attending: Brandon Bayer, Robert Rosenberg, Adam Markon, Simon Debbarma
- Brandon:
- Kitze livestream last week went great — recording on youtube
- Codebase walkthrough yesterday went great — recording on youtube
- Website overhaul, installed Theme UI
- Adam:
- Opened PR for Prisma model generation from the CLI
- Working on Installer stuff and prepping for integration with Gatsby recipes
- Simon
- Working on custom illustrations for the web
- Robert
- Misc work on website
- Website
- Most website components are owned by us now instead of docusaurus, we&#39;ll need to be weary of api updates and any other important component updates
- Make sidebar like tailwind docs sidebar
- Dark theme needs to be fixed
- Theme switcher inconsistent
- Live code sandbox examples
- Code comparison between blitz and rails
- Auth
- Rishabh continuing to work on pseudo code for the session management library
- Brandon planning to build http middleware support this week
- CLI
- Adam working on new features, including generating prisma models and making existing templates aware of actual model attributes
- Plugin ideas / discussion once recipes are farther along. Will post an RFC for plugins at some point

View File

@@ -7,4 +7,3 @@ TODO
## Reporting a Vulnerability
Email Brandon Bayer at b@bayer.ws to report a vulnerablity, and he will follow up with you asap.

View File

@@ -1,3 +0,0 @@
const {fs} = require("memfs")
module.exports = fs

1
apps/web/.eslintrc.js Normal file
View File

@@ -0,0 +1 @@
module.exports = require("@blitzjs/config/eslint")

4
apps/web/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules
# Keep environment variables out of version control
.env
*.sqlite

View File

@@ -1,12 +1,8 @@
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
```
@@ -29,6 +25,6 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next
## 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.
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_source=github.com&utm_medium=referral&utm_campaign=turborepo-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

View File

@@ -0,0 +1,20 @@
import {AuthClientPlugin} from "@blitzjs/auth"
import {setupClient} from "@blitzjs/next"
import {BlitzRpcPlugin} from "@blitzjs/rpc"
const {withBlitz} = setupClient({
plugins: [
AuthClientPlugin({
cookiePrefix: "webapp-cookie-prefix",
}),
BlitzRpcPlugin({
reactQueryOptions: {
queries: {
staleTime: 7000,
},
},
}),
],
})
export {withBlitz}

View File

@@ -0,0 +1,17 @@
import {setupBlitz} from "@blitzjs/next"
import {AuthServerPlugin, PrismaStorage} from "@blitzjs/auth"
import {prisma as db} from "../prisma/index"
import {simpleRolesIsAuthorized} from "@blitzjs/auth"
const {gSSP, gSP, api} = setupBlitz({
plugins: [
AuthServerPlugin({
cookiePrefix: "webapp-cookie-prefix",
// TODO fix type
storage: PrismaStorage(db as any),
isAuthorized: simpleRolesIsAuthorized,
}),
],
})
export {gSSP, gSP, api}

View File

@@ -0,0 +1,14 @@
import {Ctx} from "blitz"
import {prisma} from "../../prisma"
import {User} from "prisma"
export default async function createUser(
input: {name: string; email: string},
ctx: Ctx,
): Promise<User> {
ctx.session.$authorize()
const user = await prisma.user.create({data: {name: input.name, email: input.email}})
return user
}

View File

@@ -0,0 +1,4 @@
export default function setBasic(input, ctx) {
console.log("SET BASIC input", input)
return
}

View File

@@ -0,0 +1,5 @@
export default async function getBasic(input, ctx) {
console.log("INPUT", input)
return "basic-result"
}

View File

@@ -0,0 +1,11 @@
import {Ctx} from "blitz"
import {prisma} from "../../prisma"
import {User} from "prisma"
export default async function getUsers(_input: {}, ctx: Ctx): Promise<User[]> {
ctx.session.$authorize()
const users = await prisma.user.findMany()
return users
}

View File

@@ -0,0 +1,5 @@
export default function getV2Basic(input, ctx) {
console.log("INPUT", input)
return "basic-result"
}

11
apps/web/jest.config.js Normal file
View File

@@ -0,0 +1,11 @@
const nextJest = require("@blitzjs/next/jest")
const createJestConfig = nextJest({
dir: "./",
})
const customJestConfig = {
testEnvironment: "jest-environment-jsdom",
}
module.exports = createJestConfig(customJestConfig)

View File

@@ -1,5 +1,4 @@
/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited

10
apps/web/next.config.js Normal file
View File

@@ -0,0 +1,10 @@
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
const {withBlitz} = require("@blitzjs/next")
module.exports = withBlitz(
withBundleAnalyzer({
reactStrictMode: true,
}),
)

36
apps/web/package.json Normal file
View File

@@ -0,0 +1,36 @@
{
"name": "web",
"version": "0.0.0",
"private": true,
"scripts": {
"start:dev": "pnpm run prisma:start && next dev",
"buildapp": "prisma generate && next build",
"start": "next start",
"lint": "next lint",
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"prisma:start": "prisma generate && prisma migrate deploy",
"prisma:studio": "prisma studio",
"test": "jest"
},
"dependencies": {
"@blitzjs/auth": "workspace:*",
"@blitzjs/config": "workspace:*",
"@blitzjs/next": "workspace:*",
"@blitzjs/rpc": "workspace:*",
"@prisma/client": "3.9.0",
"@types/jest": "27.4.1",
"blitz": "workspace:*",
"jest": "27.5.1",
"next": "12.1.1",
"prisma": "3.9.0",
"react": "18.0.0",
"react-dom": "18.0.0",
"ts-node": "10.7.0"
},
"devDependencies": {
"@next/bundle-analyzer": "12.0.8",
"@types/react": "17.0.43",
"eslint": "7.32.0",
"typescript": "^4.5.3"
}
}

37
apps/web/pages/_app.tsx Normal file
View File

@@ -0,0 +1,37 @@
import {ErrorFallbackProps, ErrorComponent, ErrorBoundary} from "@blitzjs/next"
import {AuthenticationError, AuthorizationError} from "blitz"
import type {AppProps} from "next/app"
import React, {Suspense} from "react"
import {withBlitz} from "app/blitz-client"
function RootErrorFallback({error}: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
return <div>Error: You are not authenticated</div>
} else if (error instanceof AuthorizationError) {
return (
<ErrorComponent
statusCode={error.statusCode}
title="Sorry, you are not authorized to access this"
/>
)
} else {
return (
<ErrorComponent
statusCode={(error as any)?.statusCode || 400}
title={error.message || error.name}
/>
)
}
}
function MyApp({Component, pageProps}: AppProps) {
return (
<ErrorBoundary FallbackComponent={RootErrorFallback}>
<Suspense fallback="Loading...">
<Component {...pageProps} />
</Suspense>
</ErrorBoundary>
)
}
export default withBlitz(MyApp)

View File

@@ -0,0 +1,19 @@
import {api} from "app/blitz-server"
import {SessionContext} from "@blitzjs/auth"
import {prisma} from "../../prisma/index"
export default api(async (_req, res, ctx) => {
const blitzContext = ctx
const publicData = blitzContext.session.$publicData
const sessions = await prisma.session.findMany({})
const sessionsCount = await prisma.session.count({})
res.status(200).json({
userId: blitzContext.session.userId,
publicData: {...publicData},
activeSessions: sessions,
sessionsCount,
})
})

View File

@@ -0,0 +1,11 @@
import {api} from "app/blitz-server"
export default api(async (_req, res, ctx) => {
const {session} = ctx
const authorized = session.$isAuthorized()
res.status(200).json({
authorized,
})
})

View File

@@ -0,0 +1,9 @@
import {api} from "app/blitz-server"
export default api(async (_req, res, ctx) => {
const blitzContext = ctx
await blitzContext.session.$revoke()
res.status(200).json({userId: blitzContext.session.userId})
})

View File

@@ -0,0 +1,21 @@
import {NextApiRequest, NextApiResponse} from "next"
import {prisma} from "../../prisma/index"
export default async function handle(req: NextApiRequest, res: NextApiResponse) {
const session = await prisma.session.findFirst({
where: {
handle: "test",
},
})
const isAuthorized = !!session
if (!isAuthorized) {
res.status(404).json({message: "No access"})
return
}
const x = parseInt(req.query.x as string)
const y = parseInt(req.query.y as string)
res.json({result: x * y})
}

View File

@@ -0,0 +1,12 @@
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
export default api(async (_req, res) => {
const sessions = await prisma.session.deleteMany()
const sessionsCount = await prisma.session.count()
res.status(200).json({
activeSessions: sessions,
sessionsCount,
})
})

View File

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

View File

@@ -0,0 +1,17 @@
import {setPublicDataForUser} from "@blitzjs/auth"
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
export default api(async (req, res, ctx) => {
if (ctx.session.$thisIsAuthorized()) {
ctx.session.$publicData
await prisma.user.update({
where: {id: ctx.session.userId as number},
data: {role: req.query.role as string},
})
await setPublicDataForUser(ctx.session.userId, {role: req.query.role as string})
}
res.status(200).json({userId: ctx.session.userId, role: req.query.role as string})
})

View File

@@ -0,0 +1,31 @@
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
import {SecurePassword} from "@blitzjs/auth"
export const authenticateUser = async (email: string, password: string) => {
const user = await prisma.user.findFirst({where: {email}})
if (!user) throw new Error("Authentication Error")
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 prisma.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}})
}
const {hashedPassword, ...rest} = user
return rest
}
export default api(async (req, res, ctx) => {
const blitzContext = ctx
const user = await authenticateUser(req.query.email as string, req.query.password as string)
await blitzContext.session.$create({
userId: user.id,
})
res.status(200).json({email: req.query.email, userId: blitzContext.session.userId})
})

View File

@@ -0,0 +1,22 @@
import {api} from "app/blitz-server"
import {prisma} from "../../prisma/index"
import {SecurePassword} from "@blitzjs/auth"
export default api(async (req, res, ctx) => {
const blitzContext = ctx
const hashedPassword = await SecurePassword.hash(
(req.query.password as string) || "test-password",
)
const email = (req.query.email as string) || "test" + Math.random() + "@test.com"
const user = await prisma.user.create({
data: {email, hashedPassword, role: "user"},
select: {id: true, name: true, email: true, role: true},
})
await blitzContext.session.$create({
userId: user.id,
})
res.status(200).json({userId: blitzContext.session.userId, ...user, email: req.query.email})
})

View File

@@ -0,0 +1,17 @@
const AuthPage = () => {
return (
<div>
{JSON.stringify(
{
message: "This is a page that requires authentication",
},
null,
2,
)}
</div>
)
}
AuthPage.authenticate = true
export default AuthPage

View File

@@ -0,0 +1,41 @@
import {useState} from "react"
import {SessionContext} from "@blitzjs/auth"
import {useMutation} from "@blitzjs/rpc"
import createUser from "app/mutations/createUser"
import {User} from "prisma"
function Page() {
const [name, setName] = useState("")
const [email, setEmail] = useState("")
const [createUserMutation, {error}] = useMutation(createUser)
const [newUser, setNewUser] = useState<null | User>(null)
return (
<div>
New User Form:
<form
onSubmit={async (e) => {
e.preventDefault()
const user = await createUserMutation({name, email})
setNewUser(user)
}}
>
<label>
Name:
<input type="text" value={name} onChange={(e) => setName(e.target.value)} />
</label>
<label>
Email:
<input type="text" value={email} onChange={(e) => setEmail(e.target.value)} />
</label>
<button type="submit">Create User</button>
</form>
<div style={{paddingTop: 20}}>
<div>Error: {JSON.stringify(error, null, 2)}</div>
New user: {JSON.stringify(newUser, null, 2)}
</div>
</div>
)
}
export default Page

15
apps/web/pages/index.tsx Normal file
View File

@@ -0,0 +1,15 @@
import {invoke} from "@blitzjs/rpc"
import getBasic from "app/queries/getBasic"
export const getServerSideProps = () => {
return {props: {}}
}
export default function Web() {
return (
<div>
<h1>Web</h1>
<button onClick={() => invoke(getBasic, "FROM BROWSER")}>GetBasic</button>
</div>
)
}

View File

@@ -0,0 +1,18 @@
const PageWithAuthRedirect = () => {
return (
<div>
{JSON.stringify(
{
message:
"This is a page that will redirect you to the Home page if you are authenticated",
},
null,
2,
)}
</div>
)
}
PageWithAuthRedirect.redirectAuthenticatedTo = "/"
export default PageWithAuthRedirect

View File

@@ -0,0 +1,21 @@
import {gSP} from "app/blitz-server"
export const getStaticProps = gSP(async ({ctx}) => {
return {
props: {
data: {
// userId: ctx?.session.userId,
// session: {
// id: session.userId,
// publicData: session.$publicData,
// },
},
},
}
})
function PageWithGsp({data}) {
return <div>{JSON.stringify(data, null, 2)}</div>
}
export default PageWithGsp

View File

@@ -0,0 +1,23 @@
import {SessionContext} from "@blitzjs/auth"
import {gSSP} from "app/blitz-server"
type Props = {
userId: unknown
publicData: SessionContext["$publicData"]
}
export const getServerSideProps = gSSP<Props>(async ({ctx}) => {
const {session} = ctx
return {
props: {
userId: session.userId,
publicData: session.$publicData,
},
}
})
function PageWithGssp(props: Props) {
return <div>{JSON.stringify(props, null, 2)}</div>
}
export default PageWithGssp

View File

@@ -0,0 +1,18 @@
const PageWithRedirect = () => {
return (
<div>
{JSON.stringify(
{
message:
"This page requires authentication. It will redirect you to the Home page if you are NOT authenticated",
},
null,
2,
)}
</div>
)
}
PageWithRedirect.authenticate = {redirectTo: "/"}
export default PageWithRedirect

View File

@@ -0,0 +1,6 @@
import {useAuthenticatedSession} from "@blitzjs/auth"
export default function PageWithUseAuthSession() {
useAuthenticatedSession()
return <div>This page is using useAuthenticatedSession</div>
}

View File

@@ -0,0 +1,6 @@
import {useAuthorizeIf} from "@blitzjs/auth"
export default function PageWithUseAuthorizeIf() {
useAuthorizeIf(Math.random() > 0.5)
return <div>This page is using useAuthorizeIf</div>
}

View File

@@ -0,0 +1,6 @@
import {useAuthorize} from "@blitzjs/auth"
export default function PgaeWithUseAuthorize() {
useAuthorize()
return <div>This page is using useAuthorize</div>
}

View File

@@ -0,0 +1,6 @@
import {useRedirectAuthenticated} from "@blitzjs/auth"
export default function PageWithUseRedirectAuth() {
useRedirectAuthenticated("/")
return <div>This page is using useRedirectAuthenticated</div>
}

View File

@@ -0,0 +1,6 @@
import {useSession} from "@blitzjs/auth"
export default function PgaeWithUseSession() {
const data = useSession()
return <div>{JSON.stringify({data}, null, 2)}</div>
}

View File

@@ -0,0 +1,7 @@
const PageWithoutFlicker = ({data}) => {
return <div>{JSON.stringify(data)}</div>
}
PageWithoutFlicker.suppressFirstRenderFlicker = true
export default PageWithoutFlicker

20
apps/web/pages/users.tsx Normal file
View File

@@ -0,0 +1,20 @@
import {useQuery} from "@blitzjs/rpc"
import getUsers from "app/queries/getUsers"
function UsersPage() {
const [users] = useQuery(getUsers, {})
return (
<div>
Users:
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
</div>
)
}
export default UsersPage

View File

@@ -4,4 +4,5 @@ import {PrismaClient} from "@prisma/client"
const EnhancedPrisma = enhancePrisma(PrismaClient)
export * from "@prisma/client"
export default new EnhancedPrisma()
const prisma = new EnhancedPrisma()
export {prisma}

View File

@@ -6,7 +6,7 @@ CREATE TABLE "User" (
"name" TEXT,
"email" TEXT NOT NULL,
"hashedPassword" TEXT,
"role" TEXT NOT NULL DEFAULT 'USER'
"role" TEXT NOT NULL DEFAULT 'user'
);
-- CreateTable
@@ -16,11 +16,11 @@ CREATE TABLE "Session" (
"updatedAt" DATETIME NOT NULL,
"expiresAt" DATETIME,
"handle" TEXT NOT NULL,
"userId" INTEGER,
"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
);

View File

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

View File

@@ -1,24 +1,12 @@
// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource sqlite {
provider = "sqlite"
url = "file:./db.sqlite"
}
// SQLite is easy to start with, but if you use Postgres in production
// you should also use it in development with the following:
//datasource postgresql {
// provider = "postgresql"
// url = env("DATABASE_URL")
//}
generator client {
provider = "prisma-client-js"
}
// --------------------------------------
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
@@ -60,11 +48,3 @@ model Token {
@@unique([hashedToken, type])
}
model Project {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
dueDate DateTime?
}

View File

@@ -0,0 +1,7 @@
function sum(a, b) {
return a + b
}
test("adds 1 + 2 to equal 3", () => {
expect(sum(1, 2)).toBe(3)
})

8
apps/web/tsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"extends": "@blitzjs/config/tsconfig.nextjs.json",
"compilerOptions": {
"baseUrl": "."
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "jest.config.js"],
"exclude": ["node_modules", "test"]
}

View File

@@ -1,64 +0,0 @@
module.exports = {
presets: [
"@babel/preset-typescript",
"@babel/preset-react",
[
"@babel/preset-env",
{
modules: false,
loose: true,
exclude: [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator",
],
},
],
],
plugins: [
"babel-plugin-annotate-pure-calls",
"babel-plugin-dev-expression",
["@babel/plugin-proposal-class-properties", {loose: true}],
"babel-plugin-macros",
[
"transform-inline-environment-variables",
{
include: ["BLITZ_PROD_BUILD"],
},
],
],
overrides: [
{
test: "./test/**/*",
presets: [
[
"@babel/preset-env",
{
modules: false,
exclude: [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator",
],
},
],
"blitz/babel",
],
plugins: [],
},
{
test: "./nextjs/test/**/*",
presets: [
[
"@babel/preset-env",
{
modules: false,
exclude: [
"@babel/plugin-transform-async-to-generator",
"@babel/plugin-transform-regenerator",
],
},
],
"blitz/babel",
],
},
],
}

View File

@@ -1,3 +0,0 @@
module.exports = {
extends: ["blitz"],
}

View File

@@ -1,57 +0,0 @@
# dependencies
node_modules
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.pnp.*
.npm
web_modules/
# blitz
/.blitz/
/.next/
*.sqlite
.now
.blitz-console-history
blitz-log.log
# misc
.DS_Store
# local env files
.env
.envrc
.env.local
.env.development.local
.env.test.local
.env.production.local
# Logs
logs
*.log
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Testing
coverage
*.lcov
.nyc_output
lib-cov
# Caches
*.tsbuildinfo
.eslintcache
.node_repl_history
.yarn-integrity
# Serverless directories
.serverless/
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
.vercel

View File

@@ -1 +0,0 @@
save-exact=true

View File

@@ -1,5 +0,0 @@
.gitkeep
.env*
*.ico
*.lock
db/migrations

View File

@@ -1,2 +0,0 @@
.blitz
*.sqlite

View File

@@ -1,26 +0,0 @@
# auth
## Getting Started
1. Add this code to db/schema.prisma:
```
model Project {
id Int @default(autoincrement()) @id
name String
}
```
2. DB migrate
```
blitz prisma migrate dev
```
3. Start the dev server
```
blitz dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,145 +0,0 @@
import {passportAuth, PublicData} from "blitz"
import db from "db"
import {Strategy as TwitterStrategy} from "passport-twitter"
import {Strategy as GitHubStrategy} from "passport-github2"
import {Strategy as Auth0Strategy} from "passport-auth0"
import {Role} from "types"
function assert(condition: any, message: string): asserts condition {
if (!condition) throw new Error(message)
}
// These aren't required, but this is a good practice to ensure you have the env vars you need
assert(process.env.TWITTER_CONSUMER_KEY, "You must provide the TWITTER_CONSUMER_KEY env variable")
assert(
process.env.TWITTER_CONSUMER_SECRET,
"You must provide the TWITTER_CONSUMER_SECRET env variable",
)
assert(process.env.GITHUB_CLIENT_ID, "You must provide the GITHUB_CLIENT_ID env variable")
assert(process.env.GITHUB_CLIENT_SECRET, "You must provide the GITHUB_CLIENT_SECRET env variable")
assert(process.env.AUTH0_DOMAIN, "You must provide the AUTH0_DOMAIN env variable")
assert(process.env.AUTH0_CLIENT_ID, "You must provide the AUTH0_CLIENT_ID env variable")
assert(process.env.AUTH0_CLIENT_SECRET, "You must provide the AUTH0_CLIENT_SECRET env variable")
export default passportAuth(({ctx, req, res}) => ({
successRedirectUrl: "/",
strategies: [
{
strategy: new TwitterStrategy(
{
consumerKey: process.env.TWITTER_CONSUMER_KEY as string,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/twitter/callback"
: "http://localhost:3000/api/auth/twitter/callback",
includeEmail: true,
},
async function (_token, _tokenSecret, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("Twitter OAuth response doesn't have email."))
}
console.log(ctx.session.userId)
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {userId: user.id, role: user.role, source: "twitter"}
done(null, {publicData})
},
),
},
{
strategy: new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/github/callback"
: "http://localhost:3000/api/auth/github/callback",
},
async function (
_token: string,
_tokenSecret: string,
profile: any,
done: (err: Error | null, data?: {publicData: PublicData}) => void,
) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("Twitter OAuth response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
role: user.role as Role,
source: "github",
githubUsername: profile.username,
}
done(null, {publicData})
},
),
},
{
authenticateOptions: {scope: "openid email profile"},
strategy: new Auth0Strategy(
{
domain: process.env.AUTH0_DOMAIN as string,
clientID: process.env.AUTH0_CLIENT_ID as string,
clientSecret: process.env.AUTH0_CLIENT_SECRET as string,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/auth0/callback"
: "http://localhost:3000/api/auth/auth0/callback",
},
async function (_token, _tokenSecret, _extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
// This can happen if you haven't enabled email access in your twitter app permissions
return done(new Error("Auth0 response doesn't have email."))
}
const user = await db.user.upsert({
where: {email},
create: {
email,
name: profile.displayName,
},
update: {email},
})
const publicData = {
userId: user.id,
role: user.role,
source: "auth0",
githubUsername: profile.username,
}
done(undefined, {publicData})
},
),
},
],
}))

View File

@@ -1,56 +0,0 @@
import {AuthenticationError, Link, useMutation} from "blitz"
import {LabeledTextField} from "app/core/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/core/components/Form"
import login from "app/auth/mutations/login"
import {Login} from "app/auth/validations"
import {Routes} from ".blitz"
type LoginFormProps = {
onSuccess?: () => 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 {
await loginMutation(values)
props.onSuccess?.()
} 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>
<Link href={Routes.ForgotPasswordPage()}>
<a>Forgot your password?</a>
</Link>
</div>
</Form>
<div style={{marginTop: "1rem"}}>
Or <Link href={Routes.SignupPage()}>Sign Up</Link>
</div>
</div>
)
}
export default LoginForm

View File

@@ -1,44 +0,0 @@
import React from "react"
import {useMutation} from "blitz"
import {LabeledTextField} from "app/core/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/core/components/Form"
import signup from "app/auth/mutations/signup"
import {Signup} from "app/auth/validations"
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")) {
// This error comes from Prisma
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

@@ -1,23 +0,0 @@
import {NotFoundError, SecurePassword, resolver} from "blitz"
import db from "db"
import {authenticateUser} from "./login"
import {ChangePassword} from "../validations"
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)
await db.user.update({
where: {id: user.id},
data: {hashedPassword},
})
return true
},
)

View File

@@ -1,56 +0,0 @@
import {hash256, Ctx} from "blitz"
import forgotPassword from "./forgotPassword"
import db from "db"
import previewEmail from "preview-email"
beforeEach(async () => {
await db.$reset()
})
const generatedToken = "plain-token"
jest.mock("next/stdlib-server", () => ({
...jest.requireActual<object>("next/stdlib-server")!,
generateToken: () => generatedToken,
}))
jest.mock("preview-email", () => jest.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]
// 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

@@ -1,41 +0,0 @@
import {resolver, generateToken, hash256} from "blitz"
import db from "db"
import {forgotPasswordMailer} from "mailers/forgotPasswordMailer"
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
await forgotPasswordMailer({to: user.email, token}).send()
} 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

@@ -1,37 +0,0 @@
import {resolver, SecurePassword, AuthenticationError} from "blitz"
import db from "db"
import {Login} from "../validations"
import {Role} from "types"
export const authenticateUser = async (email: string, password: string) => {
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)
await ctx.session.$create({userId: user.id, role: user.role as Role})
return user
})
export const config = {
api: {
bodyParser: {
sizeLimit: "2mb",
},
},
}

View File

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

View File

@@ -1,82 +0,0 @@
import resetPassword from "./resetPassword"
import db from "db"
import {hash256, SecurePassword} from "blitz"
beforeEach(async () => {
await db.$reset()
})
const mockCtx: any = {
session: {
$create: jest.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

@@ -1,47 +0,0 @@
import {resolver, SecurePassword, hash256} from "blitz"
import db from "db"
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 resolver.pipe(resolver.zod(ResetPassword), async ({password, token}, ctx) => {
// 1. Try to find this token in the database
const hashedToken = hash256(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(password)
const user = await db.user.update({
where: {id: savedToken.userId},
data: {hashedPassword},
})
// 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}, ctx)
return true
})

View File

@@ -1,15 +0,0 @@
import {resolver, SecurePassword} from "blitz"
import db from "db"
import {Signup} from "app/auth/validations"
import {Role} from "types"
export default resolver.pipe(resolver.zod(Signup), async ({email, password}, ctx) => {
const hashedPassword = await SecurePassword.hash(password)
const user = await db.user.create({
data: {email: email.toLowerCase(), hashedPassword, role: "user"},
select: {id: true, name: true, email: true, role: true},
})
await ctx.session.$create({userId: user.id, role: user.role as Role})
return user
})

View File

@@ -1,47 +0,0 @@
import {BlitzPage, useMutation} from "blitz"
import Layout from "app/core/layouts/Layout"
import {LabeledTextField} from "app/core/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/core/components/Form"
import {ForgotPassword} from "app/auth/validations"
import forgotPassword from "app/auth/mutations/forgotPassword"
const ForgotPasswordPage: BlitzPage = () => {
const [forgotPasswordMutation, {isSuccess}] = useMutation(forgotPassword)
return (
<div>
<h1>Forgot your password?</h1>
{isSuccess ? (
<div>
<h2>Request Submitted</h2>
<p>
If your email is in our system, you will receive instructions to reset your password
shortly.
</p>
</div>
) : (
<Form
submitText="Send Reset Password Instructions"
schema={ForgotPassword}
initialValues={{email: ""}}
onSubmit={async (values) => {
try {
await forgotPasswordMutation(values)
} catch (error) {
return {
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
}
}
}}
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
</Form>
)}
</div>
)
}
ForgotPasswordPage.getLayout = (page) => <Layout title="Forgot Your Password?">{page}</Layout>
export default ForgotPasswordPage

View File

@@ -1,25 +0,0 @@
import React from "react"
import {useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import {LoginForm} from "app/auth/components/LoginForm"
import {Routes} from ".blitz"
const LoginPage: BlitzPage = () => {
const router = useRouter()
return (
<div>
<LoginForm
onSuccess={() => {
const next = router.query.next ? decodeURIComponent(router.query.next as string) : "/"
router.push(next)
}}
/>
</div>
)
}
LoginPage.redirectAuthenticatedTo = Routes.Home().pathname
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
export default LoginPage

View File

@@ -1,60 +0,0 @@
import {BlitzPage, useRouterQuery, Link, useMutation} from "blitz"
import Layout from "app/core/layouts/Layout"
import {LabeledTextField} from "app/core/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/core/components/Form"
import {ResetPassword} from "app/auth/validations"
import resetPassword from "app/auth/mutations/resetPassword"
import {Routes} from ".blitz"
const ResetPasswordPage: BlitzPage = () => {
const query = useRouterQuery()
const [resetPasswordMutation, {isSuccess}] = useMutation(resetPassword)
return (
<div>
<h1>Set a New Password</h1>
{isSuccess ? (
<div>
<h2>Password Reset Successfully</h2>
<p>
Go to the <Link href={Routes.Home()}>homepage</Link>
</p>
</div>
) : (
<Form
submitText="Reset Password"
schema={ResetPassword}
initialValues={{password: "", passwordConfirmation: "", token: query.token as string}}
onSubmit={async (values) => {
try {
await resetPasswordMutation(values)
} catch (error: any) {
if (error.name === "ResetPasswordError") {
return {
[FORM_ERROR]: error.message,
}
} else {
return {
[FORM_ERROR]: "Sorry, we had an unexpected error. Please try again.",
}
}
}
}}
>
<LabeledTextField name="password" label="New Password" type="password" />
<LabeledTextField
name="passwordConfirmation"
label="Confirm New Password"
type="password"
/>
</Form>
)}
</div>
)
}
ResetPasswordPage.getLayout = (page) => <Layout title="Reset Your Password">{page}</Layout>
export default ResetPasswordPage

View File

@@ -1,17 +0,0 @@
import {useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import {SignupForm} from "app/auth/components/SignupForm"
const SignupPage: BlitzPage = () => {
const router = useRouter()
return (
<div>
<SignupForm onSuccess={() => router.push("/")} />
</div>
)
}
SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>
export default SignupPage

View File

@@ -1,33 +0,0 @@
import {z} from "zod"
const password = z.string().min(10).max(100)
export const Signup = z.object({
email: z.string().email(),
password,
})
export const Login = z.object({
email: z.string().email(),
password: z.string(),
})
export const ForgotPassword = z.object({
email: z.string().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

@@ -1,59 +0,0 @@
import React, {ReactNode, PropsWithoutRef} from "react"
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import {z} from "zod"
import {validateZodSchema} from "blitz"
export {FORM_ERROR} from "final-form"
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: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
}
export function Form<S extends z.ZodType<any, any>>({
children,
submitText,
schema,
initialValues,
onSubmit,
...props
}: FormProps<S>) {
return (
<FinalForm
initialValues={initialValues}
validate={validateZodSchema(schema)}
onSubmit={onSubmit}
render={({handleSubmit, submitting, submitError}) => (
<form onSubmit={handleSubmit} className="form" {...props}>
{/* Form fields supplied as children are rendered here */}
{children}
{submitError && (
<div role="alert" style={{color: "red"}}>
{submitError}
</div>
)}
{submitText && (
<button type="submit" disabled={submitting}>
{submitText}
</button>
)}
<style global jsx>{`
.form > * + * {
margin-top: 1rem;
}
`}</style>
</form>
)}
/>
)
}
export default Form

View File

@@ -1,59 +0,0 @@
import React, {PropsWithoutRef} from "react"
import {useField} from "react-final-form"
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"]>
}
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({name, label, outerProps, ...props}, ref) => {
const {
input,
meta: {touched, error, submitError, submitting},
} = useField(name, {
parse: props.type === "number" ? Number : undefined,
})
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
return (
<div {...outerProps}>
<label>
{label}
<input {...input} disabled={submitting} {...props} ref={ref} />
</label>
{touched && normalizedError && (
<div role="alert" style={{color: "red"}}>
{normalizedError}
</div>
)}
<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

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

View File

@@ -1,32 +0,0 @@
import {useSession, useRouter, useMutation, Head, BlitzLayout} from "blitz"
import logout from "app/auth/mutations/logout"
const Layout: BlitzLayout<{title?: string}> = ({title, children}) => {
const session = useSession({suspense: false})
const router = useRouter()
const [logoutMutation] = useMutation(logout)
return (
<>
<Head>
<title>{title || "__name__"}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div>
{session.userId && (
<button
onClick={async () => {
await logoutMutation()
router.push("/")
}}
>
Logout
</button>
)}
<div>{children}</div>
</div>
</>
)
}
export default Layout

View File

@@ -1,19 +0,0 @@
import {Head, ErrorComponent} from "blitz"
// ------------------------------------------------------
// This page is rendered if a route match is not found
// ------------------------------------------------------
export default function Page404() {
const statusCode = 404
const title = "This page could not be found"
return (
<>
<Head>
<title>
{statusCode}: {title}
</title>
</Head>
<ErrorComponent statusCode={statusCode} title={title} />
</>
)
}

View File

@@ -1,45 +0,0 @@
import {
AppProps,
ErrorBoundary,
ErrorComponent,
AuthenticationError,
AuthorizationError,
ErrorFallbackProps,
useQueryErrorResetBoundary,
} from "blitz"
import LoginForm from "app/auth/components/LoginForm"
import {ReactQueryDevtools} from "react-query/devtools"
export default function App({Component, pageProps}: AppProps) {
const getLayout = Component.getLayout || ((page) => page)
const {reset} = useQueryErrorResetBoundary()
return (
<>
<ErrorBoundary FallbackComponent={RootErrorFallback} onReset={reset}>
{getLayout(<Component {...pageProps} />)}
</ErrorBoundary>
<ReactQueryDevtools />
</>
)
}
function RootErrorFallback({error, resetErrorBoundary}: ErrorFallbackProps) {
if (error instanceof AuthenticationError) {
return <LoginForm onSuccess={resetErrorBoundary} />
} else if (error instanceof AuthorizationError) {
return (
<ErrorComponent
statusCode={error.statusCode}
title="Sorry, you are not authorized to access this"
/>
)
} else {
return (
<ErrorComponent
statusCode={(error as any)?.statusCode || 400}
title={error.message || error.name}
/>
)
}
}

View File

@@ -1,23 +0,0 @@
import {Document, Html, DocumentHead, Main, BlitzScript /*DocumentContext*/} from "blitz"
class MyDocument extends Document {
// Only uncomment if you need to customize this behaviour
// static async getInitialProps(ctx: DocumentContext) {
// const initialProps = await Document.getInitialProps(ctx)
// return {...initialProps}
// }
render() {
return (
<Html lang="en">
<DocumentHead />
<body>
<Main />
<BlitzScript />
</body>
</Html>
)
}
}
export default MyDocument

Some files were not shown because too many files have changed in this diff Show More