1
0
mirror of synced 2026-02-08 06:00:13 -05:00

Compare commits

..

1 Commits
forking ... db

Author SHA1 Message Date
Brandon Bayer
767926b34a change db in auth example 2021-01-01 17:30:19 -05:00
7289 changed files with 8508 additions and 568603 deletions

View File

@@ -20,9 +20,7 @@
"code",
"content",
"ideas",
"review",
"test",
"doc"
"review"
]
},
{
@@ -367,8 +365,7 @@
"contributions": [
"code",
"test",
"maintenance",
"doc"
"maintenance"
]
},
{
@@ -1590,8 +1587,7 @@
"avatar_url": "https://avatars3.githubusercontent.com/u/43112535?v=4",
"profile": "http://Dal.Design",
"contributions": [
"code",
"doc"
"code"
]
},
{
@@ -1747,549 +1743,6 @@
"contributions": [
"doc"
]
},
{
"login": "linbudu599",
"name": "Linbudu",
"avatar_url": "https://avatars0.githubusercontent.com/u/48507806?v=4",
"profile": "https://linbudu.top/",
"contributions": [
"doc"
]
},
{
"login": "creimers",
"name": "C Reimers",
"avatar_url": "https://avatars0.githubusercontent.com/u/6090492?v=4",
"profile": "http://www.superservice-international.com",
"contributions": [
"doc"
]
},
{
"login": "kyken",
"name": "Tsuyoshi Osawa",
"avatar_url": "https://avatars2.githubusercontent.com/u/20137120?v=4",
"profile": "https://github.com/kyken",
"contributions": [
"code"
]
},
{
"login": "rembrandtreyes",
"name": "Rembrandt Reyes",
"avatar_url": "https://avatars1.githubusercontent.com/u/15057964?v=4",
"profile": "https://rembrandtreyes.com/",
"contributions": [
"code",
"doc",
"test"
]
},
{
"login": "doi-t",
"name": "Toshiya Doi",
"avatar_url": "https://avatars2.githubusercontent.com/u/5877477?v=4",
"profile": "https://doi-t.net",
"contributions": [
"doc"
]
},
{
"login": "koolii",
"name": "t.kuriyama",
"avatar_url": "https://avatars1.githubusercontent.com/u/3866581?v=4",
"profile": "https://www.koolii.net/",
"contributions": [
"code"
]
},
{
"login": "malkomalko",
"name": "Robert Malko",
"avatar_url": "https://avatars3.githubusercontent.com/u/763?v=4",
"profile": "https://github.com/malkomalko",
"contributions": [
"code"
]
},
{
"login": "ranjan-purbey",
"name": "Ranjan Purbey",
"avatar_url": "https://avatars3.githubusercontent.com/u/6953187?v=4",
"profile": "https://github.com/ranjan-purbey",
"contributions": [
"code"
]
},
{
"login": "tarunama",
"name": "tarunama",
"avatar_url": "https://avatars3.githubusercontent.com/u/6047881?v=4",
"profile": "https://github.com/tarunama",
"contributions": [
"code"
]
},
{
"login": "bacongravy",
"name": "David Kramer",
"avatar_url": "https://avatars3.githubusercontent.com/u/16848768?v=4",
"profile": "http://www.bacongravy.net/",
"contributions": [
"code"
]
},
{
"login": "mikeesto",
"name": "Michael Esteban",
"avatar_url": "https://avatars1.githubusercontent.com/u/21051488?v=4",
"profile": "https://mikeesto.com",
"contributions": [
"doc"
]
},
{
"login": "marina-ki",
"name": "marina",
"avatar_url": "https://avatars0.githubusercontent.com/u/54174518?v=4",
"profile": "https://github.com/marina-ki",
"contributions": [
"doc",
"code"
]
},
{
"login": "jonasthiesen",
"name": "Jonas Thiesen",
"avatar_url": "https://avatars.githubusercontent.com/u/23408018?v=4",
"profile": "https://github.com/jonasthiesen",
"contributions": [
"doc"
]
},
{
"login": "thakkaryash94",
"name": "Yash Thakkar",
"avatar_url": "https://avatars.githubusercontent.com/u/7349778?v=4",
"profile": "https://thakkaryash94.github.io/",
"contributions": [
"code"
]
},
{
"login": "rince",
"name": "Kazuma Suzuki",
"avatar_url": "https://avatars.githubusercontent.com/u/933895?v=4",
"profile": "https://github.com/rince",
"contributions": [
"design",
"code"
]
},
{
"login": "queq1890",
"name": "Yuji Matsumoto",
"avatar_url": "https://avatars.githubusercontent.com/u/32263803?v=4",
"profile": "http://queq1890.info",
"contributions": [
"doc"
]
},
{
"login": "Gim3l",
"name": "Gimel Dick",
"avatar_url": "https://avatars.githubusercontent.com/u/46765702?v=4",
"profile": "https://github.com/Gim3l",
"contributions": [
"code"
]
},
{
"login": "akbo",
"name": "Andreas Bollig",
"avatar_url": "https://avatars.githubusercontent.com/u/1926271?v=4",
"profile": "https://github.com/akbo",
"contributions": [
"code",
"doc"
]
},
{
"login": "ajmarkow",
"name": "AJ Markow",
"avatar_url": "https://avatars.githubusercontent.com/u/66390428?v=4",
"profile": "https://ajm.codes",
"contributions": [
"test",
"code"
]
},
{
"login": "wafuwafu13",
"name": "TagawaHirotaka",
"avatar_url": "https://avatars.githubusercontent.com/u/50798936?v=4",
"profile": "https://wafuwafu13.hateblo.jp/",
"contributions": [
"code",
"test"
]
},
{
"login": "merodiro",
"name": "Amr A.Mohammed",
"avatar_url": "https://avatars.githubusercontent.com/u/17033502?v=4",
"profile": "https://github.com/merodiro",
"contributions": [
"code"
]
},
{
"login": "lcswillems",
"name": "Lucas Willems",
"avatar_url": "https://avatars.githubusercontent.com/u/5437552?v=4",
"profile": "http://www.lucaswillems.com",
"contributions": [
"doc",
"code"
]
},
{
"login": "alii",
"name": "Alistair Smith",
"avatar_url": "https://avatars.githubusercontent.com/u/25351731?v=4",
"profile": "https://alistair.cloud",
"contributions": [
"code"
]
},
{
"login": "rodrigoehlers",
"name": "Rodrigo Ehlers",
"avatar_url": "https://avatars.githubusercontent.com/u/19683042?v=4",
"profile": "https://rodrigoehlers.com",
"contributions": [
"code"
]
},
{
"login": "mtford90",
"name": "Michael Ford",
"avatar_url": "https://avatars.githubusercontent.com/u/1734057?v=4",
"profile": "https://www.builtopen.com/",
"contributions": [
"code"
]
},
{
"login": "LBrian",
"name": "Brian Liu",
"avatar_url": "https://avatars.githubusercontent.com/u/3888780?v=4",
"profile": "https://brianypliu.com",
"contributions": [
"code"
]
},
{
"login": "beerose",
"name": "Aleksandra Sikora",
"avatar_url": "https://avatars.githubusercontent.com/u/9019397?v=4",
"profile": "http://aleksandra.codes",
"contributions": [
"code"
]
},
{
"login": "JuanM04",
"name": "JuanM04",
"avatar_url": "https://avatars.githubusercontent.com/u/16712703?v=4",
"profile": "https://juanm04.com",
"contributions": [
"code",
"doc"
]
},
{
"login": "arenddeboer",
"name": "Arend de Boer",
"avatar_url": "https://avatars.githubusercontent.com/u/7022204?v=4",
"profile": "https://github.com/arenddeboer",
"contributions": [
"doc"
]
},
{
"login": "fmilani",
"name": "Felipe Milani",
"avatar_url": "https://avatars.githubusercontent.com/u/1580375?v=4",
"profile": "https://github.com/fmilani",
"contributions": [
"doc"
]
},
{
"login": "jxe",
"name": "Joe Edelman",
"avatar_url": "https://avatars.githubusercontent.com/u/13018?v=4",
"profile": "http://nxhx.org",
"contributions": [
"code"
]
},
{
"login": "garytube",
"name": "Gary",
"avatar_url": "https://avatars.githubusercontent.com/u/3823504?v=4",
"profile": "https://github.com/garytube",
"contributions": [
"doc"
]
},
{
"login": "oliverloops",
"name": "Oliver Lopez ",
"avatar_url": "https://avatars.githubusercontent.com/u/33361399?v=4",
"profile": "http://oliverloops.com",
"contributions": [
"doc"
]
},
{
"login": "DecadentIpsum",
"name": "Andreas Zaralis",
"avatar_url": "https://avatars.githubusercontent.com/u/32861532?v=4",
"profile": "https://decadentIpsum.me",
"contributions": [
"doc"
]
},
{
"login": "davetorbeck",
"name": "David Torbeck",
"avatar_url": "https://avatars.githubusercontent.com/u/5829885?v=4",
"profile": "https://github.com/davetorbeck",
"contributions": [
"doc"
]
},
{
"login": "gusgard",
"name": "Gustavo Gard",
"avatar_url": "https://avatars.githubusercontent.com/u/2577356?v=4",
"profile": "https://github.com/gusgard",
"contributions": [
"doc"
]
},
{
"login": "Immortalin",
"name": "Immortalin",
"avatar_url": "https://avatars.githubusercontent.com/u/7126128?v=4",
"profile": "https://narrationbox.com",
"contributions": [
"code"
]
},
{
"login": "cristianbgp",
"name": "Cristian Granda",
"avatar_url": "https://avatars.githubusercontent.com/u/8507974?v=4",
"profile": "https://cristianbgp.com",
"contributions": [
"code"
]
},
{
"login": "deniseyu",
"name": "Denise Yu",
"avatar_url": "https://avatars.githubusercontent.com/u/8420094?v=4",
"profile": "https://deniseyu.io",
"contributions": [
"code"
]
},
{
"login": "andreadellacorte",
"name": "Andrea Della Corte",
"avatar_url": "https://avatars.githubusercontent.com/u/295683?v=4",
"profile": "http://dellacorte.me",
"contributions": [
"doc"
]
},
{
"login": "aditsachde",
"name": "Adit Sachde",
"avatar_url": "https://avatars.githubusercontent.com/u/23707194?v=4",
"profile": "http://aditsachde.com",
"contributions": [
"doc"
]
},
{
"login": "hirenchauhan2",
"name": "Hiren Chauhan",
"avatar_url": "https://avatars.githubusercontent.com/u/8999668?v=4",
"profile": "https://github.com/hirenchauhan2",
"contributions": [
"code"
]
},
{
"login": "remjx",
"name": "Mark Jackson",
"avatar_url": "https://avatars.githubusercontent.com/u/35121685?v=4",
"profile": "http://remjx.com/",
"contributions": [
"doc"
]
},
{
"login": "lewisblackburn",
"name": "Lewis Blackburn",
"avatar_url": "https://avatars.githubusercontent.com/u/51877955?v=4",
"profile": "https://lewisb.cloud/",
"contributions": [
"doc"
]
},
{
"login": "FDiskas",
"name": "Vytenis",
"avatar_url": "https://avatars.githubusercontent.com/u/468006?v=4",
"profile": "https://github.com/FDiskas",
"contributions": [
"code"
]
},
{
"login": "matthieu994",
"name": "Matthieu",
"avatar_url": "https://avatars.githubusercontent.com/u/12969089?v=4",
"profile": "https://portfolio.matthieupetit.com",
"contributions": [
"code",
"test"
]
},
{
"login": "mitchazj",
"name": "Mitchell Johnson",
"avatar_url": "https://avatars.githubusercontent.com/u/15032956?v=4",
"profile": "https://github.com/mitchazj",
"contributions": [
"code"
]
},
{
"login": "Roesh",
"name": "Roshan Manuel",
"avatar_url": "https://avatars.githubusercontent.com/u/31125563?v=4",
"profile": "https://roshan.page/",
"contributions": [
"code",
"doc"
]
},
{
"login": "kevinlangleyjr",
"name": "Kevin Langley Jr.",
"avatar_url": "https://avatars.githubusercontent.com/u/877634?v=4",
"profile": "https://kevinlangleyjr.com",
"contributions": [
"code"
]
},
{
"login": "heavygabriel",
"name": "Gabriel Picard",
"avatar_url": "https://avatars.githubusercontent.com/u/51029779?v=4",
"profile": "https://projet-test-99df0.firebaseapp.com/",
"contributions": [
"doc"
]
},
{
"login": "chenkie",
"name": "Ryan Chenkie",
"avatar_url": "https://avatars.githubusercontent.com/u/1847678?v=4",
"profile": "http://ryanchenkie.com/",
"contributions": [
"doc"
]
},
{
"login": "sbappan",
"name": "Santhosh B. Appan",
"avatar_url": "https://avatars.githubusercontent.com/u/12586088?v=4",
"profile": "https://github.com/sbappan",
"contributions": [
"doc"
]
},
{
"login": "james2406",
"name": "James Moran",
"avatar_url": "https://avatars.githubusercontent.com/u/10858584?v=4",
"profile": "http://stackoverflow.com/users/5207233/james-moran",
"contributions": [
"code",
"doc"
]
},
{
"login": "bugzpodder",
"name": "Jack Zhao",
"avatar_url": "https://avatars.githubusercontent.com/u/14841421?v=4",
"profile": "http://fb.me/yz",
"contributions": [
"code"
]
},
{
"login": "the-red",
"name": "Hisaki Akaza",
"avatar_url": "https://avatars.githubusercontent.com/u/4494300?v=4",
"profile": "https://github.com/the-red",
"contributions": [
"doc"
]
},
{
"login": "Flavyoo",
"name": "Flavio",
"avatar_url": "https://avatars.githubusercontent.com/u/14948074?v=4",
"profile": "http://flavioander.com/",
"contributions": [
"code"
]
},
{
"login": "pbteja1998",
"name": "Bhanu Teja Pachipulusu",
"avatar_url": "https://avatars.githubusercontent.com/u/17903466?v=4",
"profile": "https://bhanuteja.dev/",
"contributions": [
"code"
]
},
{
"login": "pavestru",
"name": "Pavel Struhar",
"avatar_url": "https://avatars.githubusercontent.com/u/10186479?v=4",
"profile": "https://twitter.com/pavestru",
"contributions": [
"code"
]
},
{
"login": "reo777",
"name": "Reo Ishiyama",
"avatar_url": "https://avatars.githubusercontent.com/u/42126368?v=4",
"profile": "https://in-thepink.com/",
"contributions": [
"code"
]
}
],
"contributorsPerLine": 7,

View File

@@ -1,51 +0,0 @@
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
// COPIED FROM nextjs/.eslintignore
/nextjs/**/.next/**
/nextjs/**/_next/**
/nextjs/**/dist/**
/nextjs/examples/with-typescript-eslint-jest/**
/nextjs/examples/with-kea/**
/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/test/integration/async-modules/**
/nextjs/test-timings.json

View File

@@ -1,19 +1,14 @@
module.exports = {
parser: "babel-eslint",
env: {
browser: true,
commonjs: true,
es6: true,
node: true,
},
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 6,
sourceType: "module",
ecmaFeatures: {
jsx: true,
},
project: `./tsconfig.json`,
},
plugins: ["import", "unicorn", "simple-import-sort"],
plugins: ["@typescript-eslint", "import", "unicorn", "simple-import-sort"],
extends: ["react-app"],
rules: {
"react/react-in-jsx-scope": "off", // React is always in scope with Blitz
@@ -28,6 +23,13 @@ module.exports = {
case: "kebabCase",
},
],
"@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"],
"simple-import-sort/imports": [
"warn",
{
@@ -49,48 +51,15 @@ module.exports = {
},
],
},
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
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/**"],
files: ["examples/**", "packages/gui/**", "recipes/**"],
rules: {
"import/no-default-export": "off",
"unicorn/filename-case": "off",
"@typescript-eslint/no-floating-promises": "off",
},
},
{
files: ["packages/cli/src/commands/**/*"],
rules: {
"require-await": "off",
},
},
{
files: ["**/__fixtures__/**"],
rules: {
"import/no-default-export": "off",
},
},
],
}

23
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,23 @@
---
name: Bug report
about: Something is not working right. Or error messages are unclear.
title: ''
labels: ''
assignees: ''
---
### What is the problem?
### Steps to Reproduce
1.
### Versions
```
output of `blitz --version --verbose`
```
### Other
Please include applicable logs and screenshots that show your problem.

View File

@@ -1,49 +0,0 @@
name: Bug Report
about: Something is not working right. Or error messages are unclear.
labels: "kind/bug, status/triage"
issue_body: true
body:
- type: markdown
attributes:
value: Thanks for taking the time to file a bug report! Please fill out this form as completely as possible.
- type: textarea
attributes:
label: What is the problem?
validations:
required: true
- type: textarea
attributes:
label: "Paste all your error logs here:"
value: |
```
PASTE_HERE (leave the ``` marks)
```
validations:
required: true
- type: textarea
attributes:
label: "Paste all relevant code snippets here:"
value: |
```
PASTE_HERE (leave the ``` marks)
```
validations:
required: true
- type: textarea
attributes:
label: What are detailed steps to reproduce this?
value: "1."
validations:
required: true
- type: textarea
attributes:
label: "Run `blitz -v` and paste the output here:"
value: |
```
PASTE_HERE (leave the ``` marks)
```
validations:
required: true
- type: markdown
attributes:
value: "Please include below any other applicable logs and screenshots that show your problem:"

View File

@@ -1,8 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Prisma issue?
url: https://github.com/prisma/prisma/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc
about: All Prisma issues should be opened in the Prisma Github
- name: Question, Discussion, Idea?
- name: Question or Discussion
url: https://github.com/blitz-js/blitz/discussions/new
about: Ask questions and discuss with other community members

View File

@@ -2,7 +2,7 @@
name: Feature/change request
about: Something new or better!
title: ""
labels: "status/triage"
labels: ""
assignees: ""
---

View File

@@ -4,7 +4,7 @@ Closes: ??
### Checklist
- [ ] Changes covered by tests (tests added if needed)
- [ ] Tests added for changes
- [ ] PR submitted to [blitzjs.com](https://github.com/blitz-js/blitzjs.com) for any user facing changes
<!-- IMPORTANT: Make sure to check the "Allow edits from maintainers" box below this window -->

View File

@@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ canary, master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ canary ]
schedule:
- cron: '31 19 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'javascript' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

View File

@@ -1,4 +1,4 @@
name: Blitz
name: CI
on:
push:
@@ -39,10 +39,6 @@ jobs:
run: yarn install --frozen-lockfile --silent
env:
CI: true
- name: manypkg lint
run: yarn manypkg check
env:
CI: true
- name: yarn lint
run: yarn lint
env:
@@ -52,13 +48,14 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node_version: [12, 14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: "15"
node-version: ${{ matrix.node_version }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
@@ -94,7 +91,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node_version: [12, 15]
node_version: [12, 14]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2

View File

@@ -1,270 +0,0 @@
name: Nextjs
defaults:
run:
working-directory: nextjs
on:
push:
branches: [canary]
pull_request:
types: [opened, synchronize]
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./
env:
NEXT_TELEMETRY_DISABLED: 1
outputs:
docsChange: ${{ steps.docs-change.outputs.DOCS_CHANGE }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
- run: yarn install --frozen-lockfile --check-files
- run: yarn workspace @blitzjs/next prepublish
- run: node run-tests.js --timings --write-timings -g 1/1
working-directory: nextjs
- name: Check docs only change
working-directory: nextjs
run: echo ::set-output name=DOCS_CHANGE::$(node skip-docs-change.js echo 'not-docs-only-change')
id: docs-change
- run: echo ${{steps.docs-change.outputs.DOCS_CHANGE}}
- uses: actions/cache@v2
id: cache-build
with:
path: ./*
key: ${{ github.sha }}
checkPrecompiled:
name: Check Pre-compiled
runs-on: ubuntu-latest
needs: build
env:
NEXT_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
- run: ./check-pre-compiled.sh
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testUnit:
name: Test Unit
runs-on: ubuntu-latest
needs: build
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
steps:
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
- run: node run-tests.js --timings --type unit -g 1/1
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testIntegration:
name: Test Integration
runs-on: ubuntu-latest
needs: build
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
strategy:
fail-fast: false
matrix:
group: [1, 2, 3, 4, 5, 6]
steps:
- run: echo ${{needs.build.outputs.docsChange}}
working-directory: ./
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
# TODO: remove after we fix watchpack watching too much
- run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
- run: xvfb-run node run-tests.js --timings -g ${{ matrix.group }}/6 -c 3
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testElectron:
name: Test Electron
runs-on: ubuntu-latest
needs: build
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
TEST_ELECTRON: 1
steps:
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
# TODO: remove after we fix watchpack watching too much
- run: echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf && sudo sysctl -p
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
- run: yarn add -W --dev spectron@7.0.0 electron@5.0.0
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
working-directory: ./
- run: xvfb-run node run-tests.js test/integration/with-electron/test/index.test.js
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testsPass:
name: thank you, next
runs-on: ubuntu-latest
needs: [checkPrecompiled, testIntegration, testUnit]
steps:
- run: exit 0
testFutureDependencies:
name: Webpack 5 (Basic, Production, Acceptance)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
NEXT_PRIVATE_TEST_WEBPACK5_MODE: 1
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- run: echo ::set-output name=DOCS_CHANGE::$(node skip-docs-change.js echo 'not-docs-only-change')
id: docs-change
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: yarn install --check-files
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: xvfb-run node run-tests.js test/integration/{fallback-modules,link-ref,production,basic,async-modules,font-optimization,ssr-ctx}/test/index.test.js test/acceptance/*.test.js
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
working-directory: nextjs
testLegacyReact:
name: React 16 + Webpack 4 (Basic, Production, Acceptance)
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./
env:
NEXT_TELEMETRY_DISABLED: 1
NEXT_TEST_JOB: 1
HEADLESS: true
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 25
- run: echo ::set-output name=DOCS_CHANGE::$(node skip-docs-change.js echo 'not-docs-only-change')
id: docs-change
- run: git fetch --depth=1 origin +refs/tags/*:refs/tags/*
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: cat package.json | jq '.resolutions.react = "^16.14.0"' > package.json.tmp && mv package.json.tmp package.json
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: cat package.json | jq '.resolutions."react-dom" = "^16.14.0"' > package.json.tmp && mv package.json.tmp package.json
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: yarn install --check-files
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: yarn list react react-dom
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
- run: xvfb-run node run-tests.js test/integration/{link-ref,production,basic,async-modules,font-optimization,ssr-ctx,worker-loader}/test/index.test.js test/acceptance/*.test.js
if: ${{ steps.docs-change.outputs.DOCS_CHANGE != 'docs only change' }}
working-directory: nextjs
testFirefox:
name: Test Firefox (production)
runs-on: ubuntu-latest
needs: build
env:
HEADLESS: true
BROWSERNAME: "firefox"
NEXT_TELEMETRY_DISABLED: 1
steps:
- uses: actions/cache@v2
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
- run: node run-tests.js test/integration/production/test/index.test.js
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testSafari:
name: Test Safari (production)
runs-on: ubuntu-latest
needs: build
env:
BROWSERSTACK: true
BROWSERNAME: "safari"
NEXT_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
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
- run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js test/integration/production/test/index.test.js'
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
testSafariOld:
name: Test Safari 10.1 (nav)
runs-on: ubuntu-latest
needs: [build, testSafari]
env:
BROWSERSTACK: true
LEGACY_SAFARI: true
BROWSERNAME: "safari"
NEXT_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
if: ${{needs.build.outputs.docsChange != 'docs only change'}}
id: restore-build
with:
path: ./*
key: ${{ github.sha }}
- run: '[[ -z "$BROWSERSTACK_ACCESS_KEY" ]] && echo "Skipping for PR" || node run-tests.js test/integration/production-nav/test/index.test.js'
if: ${{needs.build.outputs.docsChange != 'docs only change'}}

6
.gitignore vendored
View File

@@ -1,7 +1,7 @@
.log
.DS_Store
.idea
.jest-*
lib
node_modules
reports
*.log
@@ -24,7 +24,3 @@ dist
.tsbuildinfo
.nvmrc
**/.test*
examples/auth2
.idea
.ultra.cache.json
db.sqlite-journal

1
.husky/.gitignore vendored
View File

@@ -1 +0,0 @@
_

View File

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

View File

@@ -19,4 +19,3 @@ bin
!packages/blitz/src/bin
packages/generator/templates/**
.github/ISSUE_TEMPLATE/bug_report.md
nextjs/.github/workflows/**

11
GOVERNANCE.md Normal file
View File

@@ -0,0 +1,11 @@
# Blitz.js Governance
_From Brandon Bayer (@flybayer), the creator:_
Currently at this very early stage it's basically a [BDFL situation](https://opensource.guide/leadership-and-governance/#what-are-some-of-the-common-governance-structures-for-open-source-projects), with me having the final say in decisions.
However we will move away from BDFL to something that looks more like Ember.js. It's extremely important to me (Brandon) that Blitz.js is a long-term, sustainable, and community-run project.
I would love some mentorship from people with experience in large open-source projects on making this transition.
Also, it's possible I will create one or more business around Blitz, perhaps similar to how Taylor Otwell has around Laravel, but Blitz itself will always remain a separate community-run project.

View File

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

1
MAINTAINERS.md Normal file
View File

@@ -0,0 +1 @@
This document has moved here: https://blitzjs.com/docs/maintainers

3
MANIFESTO.md Normal file
View File

@@ -0,0 +1,3 @@
# Manifesto
[The Manifesto has been moved to Blitzjs.com](https://blitzjs.com/docs/manifesto)

148
README.md
View File

@@ -2,11 +2,11 @@
<!-- prettier-ignore-start -->
<p align="center">
<a aria-label="Join our Discord Community" href="https://discord.blitzjs.com">
<a aria-label="Join our Slack Community" href="https://slack.blitzjs.com">
<img alt="" src="https://img.shields.io/badge/Join%20our%20community-6700EB.svg?style=for-the-badge&labelColor=000000&logoWidth=20&logo=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAQ9SURBVHgB7d3dVdtAEIbhcSpICUoH0IEogQqSVBBSAU4FSSpIOoAORAfQgSghHXzZ1U/YcMD4R9rZmf2ec3y448LyiNf27iLiGIAmPLrweC9Un3DhrzG6EarLNP09nlwJ1SOZ/lQr5N80/S/p2QMVCBf5N17XCfm1Y/rBHqjAG9PPHvBsz+mf9WAP+HLA9M/YA14cOP2payH7jpj+VCtk1wnTP+vj7xCy6cTpn7EHLMLp059iD1iD8eveJbVCNsSLheX1YA/YgOWnf8YeKB3Wmf7Ud6Fy4f/FHmtpxbl3YlC4MJ/Cj0bWdwPnPbARg+L0S54XQHS32WwuxClzd4CM0z9rPfeAuTtA5ulPXYQ7wZ04Y+oOoDD9KZc9YOoOoDj9s4dwFzgXR6w1wIPoOvPWA9buAHEJ173o3gWiy3AnuBUHLEbgmYwvAk1/wuM8vAgexThzbwPDkx7/DHwVXfFOxP2GmsKd4Ab6zPeAyU8CI7AHFmH2BRCBPXAyk18GzUrqAXCTiR4ssyj0VFw/oCU8+e+RZ33AWz6KMaYbIIWxB+JSLs1bsbkeMN0AqakHvoku9oA2sAfqBvbAQdw0QArsgb25aYBUQT3QgT2gB+yBuqGcHij2UCqXDZACe2Anlw2QYg/QAOyBuoE98CL3DZDCuK4/rh/Q7oGL6U+TOvcNkJoijN8X1C48+T+g75eQDrAH/qmqAVJgDwyqaoAUe4AGYA/UDZX3QLUNkEIZPRCd5+6BahsgVUgPROwBTSijB7jpVAvGHriHvmw9wAZ4BpX1ABvgmakHtPcbRuwBTWAPULgAV9D/jKDY9YRvwvgEaurD44uQHvAol7qBW7WKluVtIHiUS7GyvA0s6CiXDnxrpQfsgbqBS7GKk/2jYHCrVlGyfxTMrVo0ALdq1Q3sgSKofh0M9oA61a+D2QM0AHugbmAPqClmSRjK2apVVQ8UsySsoK1aHdgDesCtWnUDeyCrIpeFg1u3sylyWTi3btMA7IG6gT2wuuK3hoE9sKrit4YVslWLPaAN7IG6ocKt2zmY2h4O9sDiTG0PZw/QANy6XTewBxZj9ogYVHy025LMHhEz9cBn0We6B0yfERReBLfhx0/R1YQHPx/QBPbA0VwcEwf2wNFcHBPHHjiem3MC2QPHcXdSaJjA+KfgTPQ8hhfjBzHC40mhlzJ+Xq9lK4a4PCs43AVaGTed5mZq+iOXZwWHi3AnOj2wFWNcnxYe7gTxLtBKHuamP/J+Wnh8a5irB7ZC5Yk9gPX1QuXC+usHWqGyhYvUYR0a7zboUOFCNVhnk0krZAOW7wFOvzXhom2xnEbIHizTA1wEYhWW6YFGyC6c1gOcfg9wfA80Qj7g8B7g9HuCww+haIR8wf49wOn3Cvv9k8tGyC/s7gFOv3fY3QONkH+v9MBWqB7PeqDn9FcIT//kcitUn6kHOu/T/xfWzlQy3dEHhwAAAABJRU5ErkJggg==">
</a>
<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-243-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<a aria-label="All Contributors" href="#contributors-"><img alt="" src="https://img.shields.io/badge/all_contributors-184-17BB8A.svg?style=for-the-badge&labelColor=000000"></a>
<!-- ALL-CONTRIBUTORS-BADGE:END -->
<a aria-label="License" href="https://github.com/blitz-js/blitz/blob/canary/LICENSE">
<img alt="" src="https://img.shields.io/npm/l/blitz.svg?style=for-the-badge&labelColor=000000&color=blue">
@@ -22,15 +22,15 @@
<h1 align="center">The Fullstack React Framework</h1>
<h5 align="center">"Zero-API" Data Layer — Built on Next.js — Inspired by Ruby on Rails</h3>
<h3 align="center">Makes you far more productive than you ever dreamed was possible 😉</h3>
<h3 align="center"><a href="https://blitzjs.com" target="_blank">Read the Documentation</a></h3>
<br>
“Zero-API” data layer **lets you import server code directly into your React components** instead of having to manually add API endpoints and do client-side fetching and caching.
New Blitz apps come with **all the boring stuff already set up for you!** Like ESLint, Prettier, Jest, user sign up, log in, and password reset.
Provides **helpful defaults and conventions** for things like routing, file structure, and authentication while also being extremely flexible.
Includes everything you need for production apps. **Everything end-to-end from the database to the frontend.**
Blitz brings back the **simplicity and conventions** of server-rendered frameworks like Ruby on Rails while preserving everything we love about React and client-side rendering!
<br>
@@ -48,8 +48,8 @@ _You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_
1. `blitz new myAppName`
2. `cd myAppName`
3. `blitz dev`
4. View your brand new app at http://localhost:3000
3. `blitz start`
4. View your baby app at http://localhost:3000
<br><br>
@@ -57,10 +57,32 @@ _You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_
<img alt="Bytes Newsletter" src="https://files-8wtskjofb.vercel.app/smarter-16x1.jpg">
</a>
<br><br>
![Architecture diagram](https://blitzjs.now.sh/img/architecture-diagram.png)
<br><br>
**Features:**<br>
⚡️ Built on Next.js<br>
⚡️ Don't have to build an API for client-side rendering<br>
⚡️ Client-side rendering, Server-side rendering, and fully static pages all in the same app<br>
⚡️ Full Typescript support with static, end-to-end typing (no code generation step needed like with GraphQL)<br>
⚡️ React Concurrent Mode enabled<br>
⚡️ Database/ORM agnostic, but Prisma 2 is default<br>
⚡️ CLI with code scaffolding, Rails-style console REPL, etc<br>
⚡️ GraphQL Ready<br>
⚡️ Deploy serverless or serverful<br>
⚡️ Highly secure authentication <br>
⚡️ Authorization you can use on both server and client<br>
⚡️ Recipes for easily adding libraries like Tailwind, CSS-in-JS, etc.<br>
**Other key features coming:**<br>
⚡️ Model validation you can use on both server and client<br>
⚡️ React native support<br>
⚡️ GUI so you don't have to use the CLI<br>
<br>
### The Foundational Principles
@@ -88,7 +110,7 @@ While we currently only support web, we are pursuing the dream of a single monol
The Blitz community is warm, safe, diverse, inclusive, and fun! LGBTQ+, women, and minorities are especially welcome. Please read our [Code of Conduct](https://blitzjs.com/docs/code-of-conduct).
[Join our Discord Community](https://discord.blitzjs.com) where we help each other build Blitz apps. It's also where we collaborate on building Blitz itself.
[Join our Slack Community](https://slack.blitzjs.com) where we help each other build Blitz apps. It's also where we collaborate on building Blitz itself.
For questions and longer form discussions, [post in our forum](https://github.com/blitz-js/blitz/discussions).
@@ -106,38 +128,37 @@ Your financial contributions help ensure Blitz continues to be developed and mai
### 🌱 Seedling Sponsors
<a aria-label="React Bricks" href="https://reactbricks.com/?utm_source=blitzjs&utm_medium=sponsorship&utm_campaign=blitzjs_sponsorship">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/reactbricks_icon.svg" width="40px"/>
<img alt="" src="https://reactbricks.com/reactbricks_icon.svg" width="30px"/>
</a>
<a aria-label="Andreas Asprou" href="https://andreas.fyi">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/andreas.jpg" width="40px"/>
</a>
<a aria-label="Robert Malko" href="https://github.com/malkomalko">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/rob_blitz.jpg" width="40px"/>
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/andreas.jpg" width="30px"/>
</a>
### 🥉 Bronze Sponsors
<a aria-label="Render.com" href="https://render.com?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/render-logo-color2.png" width="200px">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/render-logo-color2.png" width="110px">
</a>
### 🥈 Silver Sponsors
<a aria-label="Fauna" href="https://dashboard.fauna.com/accounts/register?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="300px">
<img alt="" src="https://raw.githubusercontent.com/blitz-js/blitz/canary/assets/Fauna_Logo_Blue.png" width="200px">
</a>
### 🏆 Gold Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="400px">
<div>
<a aria-label="G2i" href="http://g2i.co/sign-up?utm_source=blitz&utm_medium=referral&utm_campaign=blitz2020">
<img alt="" src="https://files-5oz00y7xp.vercel.app/G2i_Logo_wwords.png" width="160px">
</a>
</div>
### 💎 Diamond Sponsors
<a aria-label="Your Company" href="#">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="500px">
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="400px">
</a>
<br>
@@ -158,7 +179,7 @@ Your financial contributions help ensure Blitz continues to be developed and mai
## Maintainers (Level 2) ✨
_Code ownership, pull request approvals and merging, etc_ (see [Maintainers L2](https://blitzjs.com/docs/maintainers#level-2-maintainers))
_Code ownership, pull request approvals and merging, etc_ (see [MAINTAINERS.md](./MAINTAINERS.md))
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
@@ -167,7 +188,6 @@ _Code ownership, pull request approvals and merging, etc_ (see [Maintainers L2](
<td align="center"><a href="https://github.com/aem"><img src="https://avatars0.githubusercontent.com/u/1909883?v=4" width="100px;" alt=""/><br /><sub><b>Adam Markon</b></sub></a><br />CLI</td>
<td align="center"><a href="http://robdrosenberg.com"><img src="https://avatars0.githubusercontent.com/u/20813991?v=4" width="100px;" alt=""/><br /><sub><b>Robert Rosenberg</b></sub></a><br />Website/Docs</td>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br />SuperJSON</td>
<td align="center"><a href="https://juanm04.com"><img src="https://avatars0.githubusercontent.com/u/16712703?v=4" width="100px;" alt=""/><br /><sub><b>Juan Martín Seery</b></sub></a><br />Website/Docs</td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -177,25 +197,26 @@ _Code ownership, pull request approvals and merging, etc_ (see [Maintainers L2](
## Maintainers (Level 1) ✨
_Issue triage, pull request triage, community encouragement and moderation, etc_ (see [Maintainers L1](https://blitzjs.com/docs/maintainers#level-1-maintainers))
_Issue triage, pull request triage, community encouragement and moderation, etc_ (see [MAINTAINERS.md](./MAINTAINERS.md))
<!-- prettier-ignore-start -->
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://github.com/LoriKarikari"><img src="https://avatars1.githubusercontent.com/u/7902980?v=4" width="100px;" alt=""/><br /><sub><b>Lori Karikari</b></sub></a></td>
<td align="center"><a href="https://corey-brown.com"><img src="https://avatars1.githubusercontent.com/u/12791148?v=4" width="100px;" alt=""/><br /><sub><b>Corey Brown</b></sub></a></td>
<td align="center"><a href="http://jeremyliberman.com/"><img src="https://avatars3.githubusercontent.com/u/2754163?v=4" width="100px;" alt=""/><br /><sub><b>Jeremy Liberman</b></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a></td>
<td align="center"><a href="https://twitter.com/nitaking_"><img src="https://avatars2.githubusercontent.com/u/10850034?v=4" width="100px;" alt=""/><br /><sub><b>Satoshi Nitawaki</b></sub></a></td>
<td align="center"><a href="https://twitter.com/sandulat"><img src="https://avatars2.githubusercontent.com/u/7345874?v=4" width="100px;" alt=""/><br /><sub><b>Alexandru Stratulat</b></sub></a></td>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars0.githubusercontent.com/u/36962022?s=460&u=34cfc4a3d6da0a87026f6068c371779c68daa3a2&v=4" width="100px;" alt=""/><br /><sub><b>Patrick Engelkes</b></sub></a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars0.githubusercontent.com/u/36962022?s=460&u=34cfc4a3d6da0a87026f6068c371779c68daa3a2&v=4" width="100px;" alt=""/><br /><sub><b>Patrick Engelkes</b></sub></a></td>
<td align="center"><a href="https://twitter.com/jdavenport97"><img src="https://avatars2.githubusercontent.com/u/1329874?v=4" width="100px;" alt=""/><br /><sub><b>Jamie Davenport</b></sub></a></td>
<td align="center"><a href="https://twitter.com/myrondavis"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>Myron Davis</b></sub></a></td>
<td align="center"><a href="https://flavioander.com/"><img src="https://avatars2.githubusercontent.com/u/14948074?s=460&u=31d7ea58b5c5cd9f724d684ed578f68896c4af71&v=4" width="100px;" alt=""/><br /><sub><b>Flavio Andrade</b></sub></a></td>
<td align="center"><a href="https://twitter.com/NaReto1125_"><img src="https://avatars.githubusercontent.com/reo777" width="100px;" alt=""/><br /><sub><b>Reo Ishiyama</b></sub></a></td>
<td align="center"><a href="https://github.com/malkomalko"><img src="https://avatars.githubusercontent.com/malkomalko" width="100px;" alt=""/><br /><sub><b>Robert Malko</b></sub></a></td>
</tr>
</table>
<!-- markdownlint-enable -->
@@ -213,7 +234,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<!-- markdownlint-disable -->
<table>
<tr>
<td align="center"><a href="https://twitter.com/flybayer"><img src="https://avatars3.githubusercontent.com/u/8813276?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Bayer</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=flybayer" title="Code">💻</a> <a href="#content-flybayer" title="Content">🖋</a> <a href="#ideas-flybayer" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Aflybayer" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=flybayer" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=flybayer" title="Documentation">📖</a></td>
<td align="center"><a href="https://twitter.com/flybayer"><img src="https://avatars3.githubusercontent.com/u/8813276?v=4" width="100px;" alt=""/><br /><sub><b>Brandon Bayer</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=flybayer" title="Code">💻</a> <a href="#content-flybayer" title="Content">🖋</a> <a href="#ideas-flybayer" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Aflybayer" title="Reviewed Pull Requests">👀</a></td>
<td align="center"><a href="https://medium.com/@ryardley"><img src="https://avatars0.githubusercontent.com/u/1256409?v=4" width="100px;" alt=""/><br /><sub><b>Rudi Yardley</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ryardley" title="Code">💻</a> <a href="#ideas-ryardley" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Aryardley" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=ryardley" title="Tests">⚠️</a></td>
<td align="center"><a href="https://merelinguist.me"><img src="https://avatars3.githubusercontent.com/u/24858006?v=4" width="100px;" alt=""/><br /><sub><b>Dylan Brookes</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=merelinguist" title="Code">💻</a> <a href="#ideas-merelinguist" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Amerelinguist" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=merelinguist" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=merelinguist" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/aem"><img src="https://avatars0.githubusercontent.com/u/1909883?v=4" width="100px;" alt=""/><br /><sub><b>Adam Markon</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=aem" title="Code">💻</a> <a href="#ideas-aem" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Aaem" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=aem" title="Tests">⚠️</a> <a href="#maintenance-aem" title="Maintenance">🚧</a></td>
@@ -258,7 +279,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<td align="center"><a href="https://github.com/ntgussoni"><img src="https://avatars0.githubusercontent.com/u/10161067?v=4" width="100px;" alt=""/><br /><sub><b>Nicolas Torres</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ntgussoni" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3Antgussoni" title="Reviewed Pull Requests">👀</a></td>
</tr>
<tr>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Tests">⚠️</a> <a href="#maintenance-Skn0tt" title="Maintenance">🚧</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Documentation">📖</a></td>
<td align="center"><a href="http://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Skn0tt" title="Tests">⚠️</a> <a href="#maintenance-Skn0tt" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://jagascript.com"><img src="https://avatars0.githubusercontent.com/u/4562878?v=4" width="100px;" alt=""/><br /><sub><b>Jaga Santagostino</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=kandros" title="Documentation">📖</a> <a href="#maintenance-kandros" title="Maintenance">🚧</a></td>
<td align="center"><a href="http://www.joaoportela.com"><img src="https://avatars0.githubusercontent.com/u/1010018?v=4" width="100px;" alt=""/><br /><sub><b>João Portela</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jportela" title="Code">💻</a></td>
<td align="center"><a href="http://dajin.dev"><img src="https://avatars0.githubusercontent.com/u/7122182?v=4" width="100px;" alt=""/><br /><sub><b>Da-Jin Chu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dajinchu" title="Code">💻</a></td>
@@ -425,7 +446,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<td align="center"><a href="http://kattcorp.com"><img src="https://avatars1.githubusercontent.com/u/459267?v=4" width="100px;" alt=""/><br /><sub><b>Alex Johansson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=KATT" title="Code">💻</a></td>
<td align="center"><a href="http://davidmazza.com"><img src="https://avatars0.githubusercontent.com/u/120893?v=4" width="100px;" alt=""/><br /><sub><b>David Mazza</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dmzza" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rayandrews"><img src="https://avatars1.githubusercontent.com/u/4437323?v=4" width="100px;" alt=""/><br /><sub><b>Ray Andrew</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rayandrews" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=rayandrews" title="Documentation">📖</a></td>
<td align="center"><a href="http://Dal.Design"><img src="https://avatars3.githubusercontent.com/u/43112535?v=4" width="100px;" alt=""/><br /><sub><b>Abdullah Mzaien</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Mzaien" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Mzaien" title="Documentation">📖</a></td>
<td align="center"><a href="http://Dal.Design"><img src="https://avatars3.githubusercontent.com/u/43112535?v=4" width="100px;" alt=""/><br /><sub><b>Abdullah Mzaien</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Mzaien" title="Code">💻</a></td>
<td align="center"><a href="http://kwao.io"><img src="https://avatars2.githubusercontent.com/u/8839514?v=4" width="100px;" alt=""/><br /><sub><b>William Kwao</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=williamkwao" title="Documentation">📖</a></td>
</tr>
<tr>
@@ -449,81 +470,6 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
<tr>
<td align="center"><a href="https://github.com/phillipkregg"><img src="https://avatars0.githubusercontent.com/u/1066044?v=4" width="100px;" alt=""/><br /><sub><b>phillipkregg</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=phillipkregg" title="Documentation">📖</a></td>
<td align="center"><a href="http://timothyreynolds.co.uk"><img src="https://avatars1.githubusercontent.com/u/168870?v=4" width="100px;" alt=""/><br /><sub><b>Tim Reynolds</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=timReynolds" title="Documentation">📖</a></td>
<td align="center"><a href="https://linbudu.top/"><img src="https://avatars0.githubusercontent.com/u/48507806?v=4" width="100px;" alt=""/><br /><sub><b>Linbudu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=linbudu599" title="Documentation">📖</a></td>
<td align="center"><a href="http://www.superservice-international.com"><img src="https://avatars0.githubusercontent.com/u/6090492?v=4" width="100px;" alt=""/><br /><sub><b>C Reimers</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=creimers" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/kyken"><img src="https://avatars2.githubusercontent.com/u/20137120?v=4" width="100px;" alt=""/><br /><sub><b>Tsuyoshi Osawa</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kyken" title="Code">💻</a></td>
<td align="center"><a href="https://rembrandtreyes.com/"><img src="https://avatars1.githubusercontent.com/u/15057964?v=4" width="100px;" alt=""/><br /><sub><b>Rembrandt Reyes</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rembrandtreyes" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=rembrandtreyes" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=rembrandtreyes" title="Tests">⚠️</a></td>
<td align="center"><a href="https://doi-t.net"><img src="https://avatars2.githubusercontent.com/u/5877477?v=4" width="100px;" alt=""/><br /><sub><b>Toshiya Doi</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=doi-t" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://www.koolii.net/"><img src="https://avatars1.githubusercontent.com/u/3866581?v=4" width="100px;" alt=""/><br /><sub><b>t.kuriyama</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=koolii" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/malkomalko"><img src="https://avatars3.githubusercontent.com/u/763?v=4" width="100px;" alt=""/><br /><sub><b>Robert Malko</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=malkomalko" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/ranjan-purbey"><img src="https://avatars3.githubusercontent.com/u/6953187?v=4" width="100px;" alt=""/><br /><sub><b>Ranjan Purbey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ranjan-purbey" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/tarunama"><img src="https://avatars3.githubusercontent.com/u/6047881?v=4" width="100px;" alt=""/><br /><sub><b>tarunama</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tarunama" title="Code">💻</a></td>
<td align="center"><a href="http://www.bacongravy.net/"><img src="https://avatars3.githubusercontent.com/u/16848768?v=4" width="100px;" alt=""/><br /><sub><b>David Kramer</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bacongravy" title="Code">💻</a></td>
<td align="center"><a href="https://mikeesto.com"><img src="https://avatars1.githubusercontent.com/u/21051488?v=4" width="100px;" alt=""/><br /><sub><b>Michael Esteban</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mikeesto" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/marina-ki"><img src="https://avatars0.githubusercontent.com/u/54174518?v=4" width="100px;" alt=""/><br /><sub><b>marina</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=marina-ki" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=marina-ki" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/jonasthiesen"><img src="https://avatars.githubusercontent.com/u/23408018?v=4" width="100px;" alt=""/><br /><sub><b>Jonas Thiesen</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jonasthiesen" title="Documentation">📖</a></td>
<td align="center"><a href="https://thakkaryash94.github.io/"><img src="https://avatars.githubusercontent.com/u/7349778?v=4" width="100px;" alt=""/><br /><sub><b>Yash Thakkar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=thakkaryash94" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/rince"><img src="https://avatars.githubusercontent.com/u/933895?v=4" width="100px;" alt=""/><br /><sub><b>Kazuma Suzuki</b></sub></a><br /><a href="#design-rince" title="Design">🎨</a> <a href="https://github.com/blitz-js/blitz/commits?author=rince" title="Code">💻</a></td>
<td align="center"><a href="http://queq1890.info"><img src="https://avatars.githubusercontent.com/u/32263803?v=4" width="100px;" alt=""/><br /><sub><b>Yuji Matsumoto</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=queq1890" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/Gim3l"><img src="https://avatars.githubusercontent.com/u/46765702?v=4" width="100px;" alt=""/><br /><sub><b>Gimel Dick</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Gim3l" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/akbo"><img src="https://avatars.githubusercontent.com/u/1926271?v=4" width="100px;" alt=""/><br /><sub><b>Andreas Bollig</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=akbo" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=akbo" title="Documentation">📖</a></td>
<td align="center"><a href="https://ajm.codes"><img src="https://avatars.githubusercontent.com/u/66390428?v=4" width="100px;" alt=""/><br /><sub><b>AJ Markow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ajmarkow" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=ajmarkow" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://wafuwafu13.hateblo.jp/"><img src="https://avatars.githubusercontent.com/u/50798936?v=4" width="100px;" alt=""/><br /><sub><b>TagawaHirotaka</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=wafuwafu13" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=wafuwafu13" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/merodiro"><img src="https://avatars.githubusercontent.com/u/17033502?v=4" width="100px;" alt=""/><br /><sub><b>Amr A.Mohammed</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=merodiro" title="Code">💻</a></td>
<td align="center"><a href="http://www.lucaswillems.com"><img src="https://avatars.githubusercontent.com/u/5437552?v=4" width="100px;" alt=""/><br /><sub><b>Lucas Willems</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lcswillems" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=lcswillems" title="Code">💻</a></td>
<td align="center"><a href="https://alistair.cloud"><img src="https://avatars.githubusercontent.com/u/25351731?v=4" width="100px;" alt=""/><br /><sub><b>Alistair Smith</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alii" title="Code">💻</a></td>
<td align="center"><a href="https://rodrigoehlers.com"><img src="https://avatars.githubusercontent.com/u/19683042?v=4" width="100px;" alt=""/><br /><sub><b>Rodrigo Ehlers</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rodrigoehlers" title="Code">💻</a></td>
<td align="center"><a href="https://www.builtopen.com/"><img src="https://avatars.githubusercontent.com/u/1734057?v=4" width="100px;" alt=""/><br /><sub><b>Michael Ford</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mtford90" title="Code">💻</a></td>
<td align="center"><a href="https://brianypliu.com"><img src="https://avatars.githubusercontent.com/u/3888780?v=4" width="100px;" alt=""/><br /><sub><b>Brian Liu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=LBrian" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="http://aleksandra.codes"><img src="https://avatars.githubusercontent.com/u/9019397?v=4" width="100px;" alt=""/><br /><sub><b>Aleksandra Sikora</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=beerose" title="Code">💻</a></td>
<td align="center"><a href="https://juanm04.com"><img src="https://avatars.githubusercontent.com/u/16712703?v=4" width="100px;" alt=""/><br /><sub><b>JuanM04</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=JuanM04" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=JuanM04" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/arenddeboer"><img src="https://avatars.githubusercontent.com/u/7022204?v=4" width="100px;" alt=""/><br /><sub><b>Arend de Boer</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=arenddeboer" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/fmilani"><img src="https://avatars.githubusercontent.com/u/1580375?v=4" width="100px;" alt=""/><br /><sub><b>Felipe Milani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=fmilani" title="Documentation">📖</a></td>
<td align="center"><a href="http://nxhx.org"><img src="https://avatars.githubusercontent.com/u/13018?v=4" width="100px;" alt=""/><br /><sub><b>Joe Edelman</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jxe" title="Code">💻</a></td>
<td align="center"><a href="https://github.com/garytube"><img src="https://avatars.githubusercontent.com/u/3823504?v=4" width="100px;" alt=""/><br /><sub><b>Gary</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=garytube" title="Documentation">📖</a></td>
<td align="center"><a href="http://oliverloops.com"><img src="https://avatars.githubusercontent.com/u/33361399?v=4" width="100px;" alt=""/><br /><sub><b>Oliver Lopez </b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=oliverloops" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="https://decadentIpsum.me"><img src="https://avatars.githubusercontent.com/u/32861532?v=4" width="100px;" alt=""/><br /><sub><b>Andreas Zaralis</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=DecadentIpsum" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/davetorbeck"><img src="https://avatars.githubusercontent.com/u/5829885?v=4" width="100px;" alt=""/><br /><sub><b>David Torbeck</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=davetorbeck" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/gusgard"><img src="https://avatars.githubusercontent.com/u/2577356?v=4" width="100px;" alt=""/><br /><sub><b>Gustavo Gard</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=gusgard" title="Documentation">📖</a></td>
<td align="center"><a href="https://narrationbox.com"><img src="https://avatars.githubusercontent.com/u/7126128?v=4" width="100px;" alt=""/><br /><sub><b>Immortalin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Immortalin" title="Code">💻</a></td>
<td align="center"><a href="https://cristianbgp.com"><img src="https://avatars.githubusercontent.com/u/8507974?v=4" width="100px;" alt=""/><br /><sub><b>Cristian Granda</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cristianbgp" title="Code">💻</a></td>
<td align="center"><a href="https://deniseyu.io"><img src="https://avatars.githubusercontent.com/u/8420094?v=4" width="100px;" alt=""/><br /><sub><b>Denise Yu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=deniseyu" title="Code">💻</a></td>
<td align="center"><a href="http://dellacorte.me"><img src="https://avatars.githubusercontent.com/u/295683?v=4" width="100px;" alt=""/><br /><sub><b>Andrea Della Corte</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=andreadellacorte" title="Documentation">📖</a></td>
</tr>
<tr>
<td align="center"><a href="http://aditsachde.com"><img src="https://avatars.githubusercontent.com/u/23707194?v=4" width="100px;" alt=""/><br /><sub><b>Adit Sachde</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=aditsachde" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/hirenchauhan2"><img src="https://avatars.githubusercontent.com/u/8999668?v=4" width="100px;" alt=""/><br /><sub><b>Hiren Chauhan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=hirenchauhan2" title="Code">💻</a></td>
<td align="center"><a href="http://remjx.com/"><img src="https://avatars.githubusercontent.com/u/35121685?v=4" width="100px;" alt=""/><br /><sub><b>Mark Jackson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=remjx" title="Documentation">📖</a></td>
<td align="center"><a href="https://lewisb.cloud/"><img src="https://avatars.githubusercontent.com/u/51877955?v=4" width="100px;" alt=""/><br /><sub><b>Lewis Blackburn</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=lewisblackburn" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/FDiskas"><img src="https://avatars.githubusercontent.com/u/468006?v=4" width="100px;" alt=""/><br /><sub><b>Vytenis</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=FDiskas" title="Code">💻</a></td>
<td align="center"><a href="https://portfolio.matthieupetit.com"><img src="https://avatars.githubusercontent.com/u/12969089?v=4" width="100px;" alt=""/><br /><sub><b>Matthieu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=matthieu994" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=matthieu994" title="Tests">⚠️</a></td>
<td align="center"><a href="https://github.com/mitchazj"><img src="https://avatars.githubusercontent.com/u/15032956?v=4" width="100px;" alt=""/><br /><sub><b>Mitchell Johnson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mitchazj" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://roshan.page/"><img src="https://avatars.githubusercontent.com/u/31125563?v=4" width="100px;" alt=""/><br /><sub><b>Roshan Manuel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Roesh" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=Roesh" title="Documentation">📖</a></td>
<td align="center"><a href="https://kevinlangleyjr.com"><img src="https://avatars.githubusercontent.com/u/877634?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Langley Jr.</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kevinlangleyjr" title="Code">💻</a></td>
<td align="center"><a href="https://projet-test-99df0.firebaseapp.com/"><img src="https://avatars.githubusercontent.com/u/51029779?v=4" width="100px;" alt=""/><br /><sub><b>Gabriel Picard</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=heavygabriel" title="Documentation">📖</a></td>
<td align="center"><a href="http://ryanchenkie.com/"><img src="https://avatars.githubusercontent.com/u/1847678?v=4" width="100px;" alt=""/><br /><sub><b>Ryan Chenkie</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=chenkie" title="Documentation">📖</a></td>
<td align="center"><a href="https://github.com/sbappan"><img src="https://avatars.githubusercontent.com/u/12586088?v=4" width="100px;" alt=""/><br /><sub><b>Santhosh B. Appan</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sbappan" title="Documentation">📖</a></td>
<td align="center"><a href="http://stackoverflow.com/users/5207233/james-moran"><img src="https://avatars.githubusercontent.com/u/10858584?v=4" width="100px;" alt=""/><br /><sub><b>James Moran</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=james2406" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=james2406" title="Documentation">📖</a></td>
<td align="center"><a href="http://fb.me/yz"><img src="https://avatars.githubusercontent.com/u/14841421?v=4" width="100px;" alt=""/><br /><sub><b>Jack Zhao</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=bugzpodder" title="Code">💻</a></td>
</tr>
<tr>
<td align="center"><a href="https://github.com/the-red"><img src="https://avatars.githubusercontent.com/u/4494300?v=4" width="100px;" alt=""/><br /><sub><b>Hisaki Akaza</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=the-red" title="Documentation">📖</a></td>
<td align="center"><a href="http://flavioander.com/"><img src="https://avatars.githubusercontent.com/u/14948074?v=4" width="100px;" alt=""/><br /><sub><b>Flavio</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Flavyoo" title="Code">💻</a></td>
<td align="center"><a href="https://bhanuteja.dev/"><img src="https://avatars.githubusercontent.com/u/17903466?v=4" width="100px;" alt=""/><br /><sub><b>Bhanu Teja Pachipulusu</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pbteja1998" title="Code">💻</a></td>
<td align="center"><a href="https://twitter.com/pavestru"><img src="https://avatars.githubusercontent.com/u/10186479?v=4" width="100px;" alt=""/><br /><sub><b>Pavel Struhar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=pavestru" title="Code">💻</a></td>
<td align="center"><a href="https://in-thepink.com/"><img src="https://avatars.githubusercontent.com/u/42126368?v=4" width="100px;" alt=""/><br /><sub><b>Reo Ishiyama</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=reo777" title="Code">💻</a></td>
</tr>
</table>

View File

@@ -1,10 +0,0 @@
# Security Policy
## Supported Versions
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 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 980 979.97"><path d="M139.38,0c-185.82-3.54-185.82,277,0,273.46H251.44C437.25,277,437.25-3.5,251.44,0ZM729.25,353.24c-184.92-2.62-184.92,276.1,0,273.48H841.31c184.92,2.62,184.92-276.1,0-273.48ZM138.69,706.46c-184.92-2.61-184.92,276.1,0,273.49H250.75c184.91,2.61,184.91-276.1,0-273.49Z" fill="#b43278"/><path d="M583.47,0c-185.82-3.54-185.82,277,0,273.46H840.61c185.81,3.54,185.81-277,0-273.46ZM138.7,353.24c-184.92-2.62-184.92,276.1,0,273.48H395.85c184.92,2.62,184.92-276.1,0-273.48ZM584.13,706.46c-184.91-2.61-184.91,276.1,0,273.49H841.3c184.92,2.61,184.92-276.1,0-273.49Z" fill="#f65a8e"/></svg>

Before

Width:  |  Height:  |  Size: 650 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

View File

@@ -1,29 +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"],
},
],
],
}

View File

@@ -1,4 +1,4 @@
module.exports = {
presets: ["blitz/babel"],
presets: ["next/babel"],
plugins: [],
}

View File

@@ -1,3 +1,15 @@
module.exports = {
extends: ["blitz"],
env: {
es2020: true,
"cypress/globals": true,
},
extends: ["react-app", "plugin:jsx-a11y/recommended"],
plugins: ["jsx-a11y", "cypress"],
rules: {
"import/no-anonymous-default-export": "error",
"import/no-webpack-loader-syntax": "off",
"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
"jsx-a11y/label-has-associated-control": "off", //Doesn't play well with form libraries
},
}

View File

@@ -14,13 +14,13 @@ model Project {
2. DB migrate
```
blitz prisma migrate dev --preview-feature
blitz db migrate
```
3. Start the dev server
```
blitz dev
blitz start
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

View File

@@ -1,9 +1,8 @@
import {passportAuth, PublicData} from "blitz"
import {passportAuth} 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)
@@ -22,14 +21,14 @@ 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) => ({
export default passportAuth({
successRedirectUrl: "/",
strategies: [
{
strategy: new TwitterStrategy(
{
consumerKey: process.env.TWITTER_CONSUMER_KEY as string,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET as string,
consumerKey: process.env.TWITTER_CONSUMER_KEY,
consumerSecret: process.env.TWITTER_CONSUMER_SECRET,
callbackURL:
process.env.NODE_ENV === "production"
? "https://auth-example-flybayer.blitzjs.vercel.app/api/auth/twitter/callback"
@@ -44,8 +43,6 @@ export default passportAuth((ctx) => ({
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: {
@@ -55,7 +52,7 @@ export default passportAuth((ctx) => ({
update: {email},
})
const publicData = {userId: user.id, role: user.role, source: "twitter"}
const publicData = {userId: user.id, roles: [user.role], source: "twitter"}
done(null, {publicData})
},
),
@@ -63,19 +60,14 @@ export default passportAuth((ctx) => ({
{
strategy: new GitHubStrategy(
{
clientID: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
clientID: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
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,
) {
async function (_token, _tokenSecret, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
@@ -94,7 +86,7 @@ export default passportAuth((ctx) => ({
const publicData = {
userId: user.id,
role: user.role as Role,
roles: [user.role],
source: "github",
githubUsername: profile.username,
}
@@ -106,15 +98,15 @@ export default passportAuth((ctx) => ({
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,
domain: process.env.AUTH0_DOMAIN,
clientID: process.env.AUTH0_CLIENT_ID,
clientSecret: process.env.AUTH0_CLIENT_SECRET,
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) {
async function (_token, _tokenSecret, extraParams, profile, done) {
const email = profile.emails && profile.emails[0]?.value
if (!email) {
@@ -133,7 +125,7 @@ export default passportAuth((ctx) => ({
const publicData = {
userId: user.id,
role: user.role,
roles: [user.role],
source: "auth0",
githubUsername: profile.username,
}
@@ -142,4 +134,4 @@ export default passportAuth((ctx) => ({
),
},
],
}))
})

View File

@@ -2,15 +2,15 @@ import {AuthenticationError} from "blitz"
import SecurePassword from "secure-password"
import db from "db"
const SP = new SecurePassword()
const SP = () => new SecurePassword()
export const hashPassword = async (password: string) => {
const hashedBuffer = await SP.hash(Buffer.from(password))
const hashedBuffer = await SP().hash(Buffer.from(password))
return hashedBuffer.toString("base64")
}
export const verifyPassword = async (hashedPassword: string, password: string) => {
try {
return await SP.verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
return await SP().verify(Buffer.from(password), Buffer.from(hashedPassword, "base64"))
} catch (error) {
console.error(error)
return false
@@ -18,7 +18,7 @@ export const verifyPassword = async (hashedPassword: string, password: string) =
}
export const authenticateUser = async (email: string, password: string) => {
const user = await db.user.findFirst({where: {email: email.toLowerCase()}})
const user = await db().user.findFirst({where: {email}})
if (!user || !user.hashedPassword) throw new AuthenticationError()
@@ -28,7 +28,7 @@ export const authenticateUser = async (email: string, password: string) => {
case SecurePassword.VALID_NEEDS_REHASH:
// Upgrade hashed password with a more secure hash
const improvedHash = await hashPassword(password)
await db.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}})
await db().user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}})
break
default:
throw new AuthenticationError()

View File

@@ -1,8 +1,7 @@
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 {Link, useMutation, AuthenticationError} from "blitz"
import {LabeledTextField} from "app/components/LabeledTextField"
import {Form, FORM_ERROR} from "app/components/Form"
import login, {LoginInput} from "app/auth/mutations/login"
type LoginFormProps = {
onSuccess?: () => void
@@ -10,19 +9,17 @@ type LoginFormProps = {
export const LoginForm = (props: LoginFormProps) => {
const [loginMutation] = useMutation(login)
return (
<div>
<h1>Login</h1>
<Form
submitText="Login"
schema={Login}
initialValues={{email: "", password: ""}}
schema={LoginInput}
initialValues={{email: undefined, password: undefined}}
onSubmit={async (values) => {
try {
await loginMutation(values)
props.onSuccess?.()
props.onSuccess && props.onSuccess()
} catch (error) {
if (error instanceof AuthenticationError) {
return {[FORM_ERROR]: "Sorry, those credentials are invalid"}
@@ -37,13 +34,7 @@ export const LoginForm = (props: LoginFormProps) => {
>
<LabeledTextField name="email" label="Email" placeholder="Email" />
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
<div>
<Link href="/forgot-password">
<a>Forgot your password?</a>
</Link>
</div>
</Form>
<div style={{marginTop: "1rem"}}>
Or <Link href="/signup">Sign Up</Link>
</div>

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) {
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("@blitzjs/core/server", () => ({
...jest.requireActual("@blitzjs/core/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,29 +1,21 @@
import {resolver, SecurePassword, AuthenticationError} from "blitz"
import db from "db"
import {Login} from "../validations"
import {Role} from "types"
import {Ctx} from "blitz"
import {authenticateUser} from "app/auth/auth-utils"
import * as z from "zod"
export const authenticateUser = async (email: string, password: string) => {
const user = await db.user.findFirst({where: {email}})
if (!user) throw new AuthenticationError()
export const LoginInput = z.object({
email: z.string().email(),
password: z.string(),
})
export type LoginInputType = z.infer<typeof LoginInput>
const result = await SecurePassword.verify(user.hashedPassword, password)
export default async function login(input: LoginInputType, {session}: Ctx) {
// This throws an error if input is invalid
const {email, password} = LoginInput.parse(input)
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})
await session.create({userId: user.id, roles: [user.role]})
return user
})
}

View File

@@ -1,5 +1,5 @@
import {Ctx} from "blitz"
export default async function logout(_: any, ctx: Ctx) {
return await ctx.session.$revoke()
export default async function logout(_: any, {session}: Ctx) {
return await 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 +1,25 @@
import {resolver, SecurePassword} from "blitz"
import {Ctx} from "blitz"
import db from "db"
import {Signup} from "app/auth/validations"
import {Role} from "types"
import {hashPassword} from "app/auth/auth-utils"
import * as z from "zod"
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"},
export const SignupInput = z.object({
email: z.string().email(),
password: z.string().min(10).max(100),
})
export type SignupInputType = z.infer<typeof SignupInput>
export default async function signup(input: SignupInputType, {session}: Ctx) {
// This throws an error if input is invalid
const {email, password} = SignupInput.parse(input)
const hashedPassword = await hashPassword(password)
const user = await db().user.create({
data: {email, hashedPassword, role: "user"},
select: {id: true, name: true, email: true, role: true},
})
await ctx.session.$create({userId: user.id, role: user.role as Role})
await session.create({userId: user.id, roles: [user.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,24 +1,21 @@
import React from "react"
import {useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import {Head, useRouter, BlitzPage} from "blitz"
import {LoginForm} from "app/auth/components/LoginForm"
const LoginPage: BlitzPage = () => {
const SignupPage: BlitzPage = () => {
const router = useRouter()
return (
<div>
<LoginForm
onSuccess={() => {
const next = (router.query.next as string) ?? "/"
router.push(next)
}}
/>
</div>
<>
<Head>
<title>Login</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div>
<LoginForm onSuccess={() => router.push("/")} />
</div>
</>
)
}
LoginPage.redirectAuthenticatedTo = "/"
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
export default LoginPage
export default SignupPage

View File

@@ -1,58 +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"
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="/">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) {
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 +1,53 @@
import {useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import {SignupForm} from "app/auth/components/SignupForm"
import {Head, useRouter, BlitzPage, useMutation} from "blitz"
import {Form, FORM_ERROR} from "app/components/Form"
import {LabeledTextField} from "app/components/LabeledTextField"
import signup, {SignupInput} from "app/auth/mutations/signup"
const SignupPage: BlitzPage = () => {
const router = useRouter()
const [signupMutation] = useMutation(signup)
return (
<div>
<SignupForm onSuccess={() => router.push("/")} />
</div>
<>
<Head>
<title>Sign Up</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div>
<h1>Create an Account</h1>
<Form
submitText="Create Account"
schema={SignupInput}
onSubmit={async (values) => {
try {
await signupMutation(values)
router.push("/")
} catch (error) {
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]:
"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"
/>
</Form>
</div>
</>
)
}
SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>
export default SignupPage

View File

@@ -1,33 +0,0 @@
import * as 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,4 +1,4 @@
import React, {ReactNode, PropsWithoutRef} from "react"
import {ReactNode, PropsWithoutRef} from "react"
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import * as z from "zod"
export {FORM_ERROR} from "final-form"
@@ -7,10 +7,10 @@ type FormProps<S extends z.ZodType<any, any>> = {
/** All your form fields */
children: ReactNode
/** Text to display in the submit button */
submitText?: string
schema?: S
submitText: string
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
schema?: S
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
export function Form<S extends z.ZodType<any, any>>({
@@ -44,11 +44,9 @@ export function Form<S extends z.ZodType<any, any>>({
</div>
)}
{submitText && (
<button type="submit" disabled={submitting}>
{submitText}
</button>
)}
<button type="submit" disabled={submitting}>
{submitText}
</button>
<style global jsx>{`
.form > * + * {

View File

@@ -1,4 +1,4 @@
import React, {PropsWithoutRef} from "react"
import {forwardRef, PropsWithoutRef} from "react"
import {useField} from "react-final-form"
export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElements["input"]> {
@@ -11,15 +11,13 @@ export interface LabeledTextFieldProps extends PropsWithoutRef<JSX.IntrinsicElem
outerProps?: PropsWithoutRef<JSX.IntrinsicElements["div"]>
}
export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFieldProps>(
export const LabeledTextField = forwardRef<HTMLInputElement, LabeledTextFieldProps>(
({name, label, outerProps, ...props}, ref) => {
const {
input,
meta: {touched, error, submitError, submitting},
} = useField(name)
const normalizedError = Array.isArray(error) ? error.join(", ") : error || submitError
return (
<div {...outerProps}>
<label>
@@ -27,9 +25,9 @@ export const LabeledTextField = React.forwardRef<HTMLInputElement, LabeledTextFi
<input {...input} disabled={submitting} {...props} ref={ref} />
</label>
{touched && normalizedError && (
{touched && (error || submitError) && (
<div role="alert" style={{color: "red"}}>
{normalizedError}
{error || submitError}
</div>
)}

View File

@@ -1,65 +0,0 @@
import React, {ReactNode, PropsWithoutRef} from "react"
import {Form as FinalForm, FormProps as FinalFormProps} from "react-final-form"
import * as z from "zod"
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={(values) => {
if (!schema) return
try {
schema.parse(values)
} catch (error) {
return error.formErrors.fieldErrors
}
}}
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,30 +0,0 @@
import {useSession, useRouter, useMutation, Head} from "blitz"
import logout from "app/auth/mutations/logout"
export default function Layout({title, children}: {title?: string; children: React.ReactNode}) {
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 () => {
router.push("/")
await logoutMutation()
}}
>
Logout
</button>
)}
<div>{children}</div>
</div>
</>
)
}

View File

@@ -0,0 +1,23 @@
import {useSession, useRouter, useMutation} from "blitz"
import logout from "app/auth/mutations/logout"
export default function Layout({children}: {children: React.ReactNode}) {
const session = useSession()
const router = useRouter()
const [logoutMutation] = useMutation(logout)
return (
<div>
{session.userId && (
<button
onClick={async () => {
router.push("/")
await logoutMutation()
}}
>
Logout
</button>
)}
<div>{children}</div>
</div>
)
}

View File

@@ -1,19 +1,14 @@
import {
AppProps,
ErrorComponent,
useRouter,
AuthenticationError,
AuthorizationError,
ErrorFallbackProps,
} from "blitz"
import {AppProps, ErrorComponent, useRouter, AuthenticationError, AuthorizationError} from "blitz"
import {ErrorBoundary} from "react-error-boundary"
import {queryCache} from "react-query"
import LoginForm from "app/auth/components/LoginForm"
export default function App({Component, pageProps}: AppProps) {
const getLayout = Component.getLayout || ((page) => page)
const router = useRouter()
if (typeof window !== "undefined") {
window["DEBUG_BLITZ"] = 1
}
export default function App({Component, pageProps}: AppProps) {
const router = useRouter()
return (
<ErrorBoundary
FallbackComponent={RootErrorFallback}
@@ -24,12 +19,12 @@ export default function App({Component, pageProps}: AppProps) {
queryCache.resetErrorBoundaries()
}}
>
{getLayout(<Component {...pageProps} />)}
<Component {...pageProps} />
</ErrorBoundary>
)
}
function RootErrorFallback({error, resetErrorBoundary}: ErrorFallbackProps) {
function RootErrorFallback({error, resetErrorBoundary}) {
if (error instanceof AuthenticationError) {
return <LoginForm onSuccess={resetErrorBoundary} />
} else if (error instanceof AuthorizationError) {

View File

@@ -1,27 +1,20 @@
import {render} from "test/utils"
import Home from "./index"
jest.mock("@blitzjs/core", () => ({
...jest.requireActual("@blitzjs/core")!,
useQuery: () => [
{
id: 1,
name: "User",
email: "user@email.com",
role: "user",
},
],
}))
import Home from "./index"
import {useCurrentUser} from "app/hooks/useCurrentUser"
jest.mock("app/hooks/useCurrentUser")
const mockUseCurrentUser = useCurrentUser as jest.MockedFunction<typeof useCurrentUser>
test("renders blitz documentation link", () => {
// This is an example of how to ensure a specific item is in the document
// But it's disabled by default (by test.skip) so the test doesn't fail
// when you remove the the default content from the page
// This is an example on how to mock api hooks when testing
mockUseCurrentUser.mockReturnValue({
id: 1,
name: "User",
email: "user@email.com",
role: "user",
})
const {getByText} = render(<Home />)
const element = getByText(/powered by blitz/i)
// @ts-ignore
expect(element).toBeInTheDocument()
})

View File

@@ -1,30 +1,30 @@
import {Suspense} from "react"
import {
Head,
Link,
useSession,
useRouterQuery,
useMutation,
invoke,
useQuery,
BlitzPage,
} from "blitz"
import {Head, Link, useSession, useRouterQuery, useMutation, invoke} from "blitz"
import getUser from "app/users/queries/getUser"
import trackView from "app/users/mutations/trackView"
import Layout from "app/core/layouts/Layout"
import Layout from "app/layouts/Layout"
import {useCurrentUser} from "app/hooks/useCurrentUser"
// import getUsers from "app/users/queries/getUsers"
const CurrentUserInfo = () => {
const session = useSession()
const [currentUser] = useQuery(getUser, {where: {id: session.userId!}})
const currentUser = useCurrentUser()
return <pre>{JSON.stringify(currentUser, null, 2)}</pre>
}
// const Users = () => {
// const [users] = useQuery(getUsers, {})
//
// return <pre style={{maxWidth: "30rem"}}>{JSON.stringify(users, null, 2)}</pre>
// }
const UserStuff = () => {
const session = useSession()
const query = useRouterQuery()
const [trackViewMutation] = useMutation(trackView)
if (session.isLoading) return <div>Loading...</div>
return (
<div>
{!session.userId && (
@@ -48,6 +48,11 @@ const UserStuff = () => {
<Suspense fallback="Loading...">
<CurrentUserInfo />
</Suspense>
{/*
<Suspense fallback="Loading...">
<Users />
</Suspense>
*/}
<button
onClick={async () => {
try {
@@ -76,7 +81,7 @@ const UserStuff = () => {
)
}
const Home: BlitzPage = () => (
const Home = () => (
<Layout>
<div className="container">
<Head>
@@ -89,9 +94,7 @@ const Home: BlitzPage = () => (
<img src="/logo.png" alt="blitz.js" />
</div>
<Suspense fallback={"Loading..."}>
<UserStuff />
</Suspense>
<UserStuff />
</main>
<footer>
@@ -235,6 +238,4 @@ const Home: BlitzPage = () => (
</Layout>
)
Home.suppressFirstRenderFlicker = true
export default Home

View File

@@ -1,65 +0,0 @@
import {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useParam, BlitzPage, useMutation} from "blitz"
import Layout from "app/core/layouts/Layout"
import getProject from "app/projects/queries/getProject"
import deleteProject from "app/projects/mutations/deleteProject"
export const Project = () => {
const router = useRouter()
const projectId = useParam("projectId", "number")
const [deleteProjectMutation, {isSuccess}] = useMutation(deleteProject)
const [project] = useQuery(getProject, {id: projectId}, {enabled: !isSuccess})
if (!project) return null
return (
<>
<Head>
<title>Project {project.id}</title>
</Head>
<div>
<h1>Project {project.id}</h1>
<pre>{JSON.stringify(project, null, 2)}</pre>
<Link href={`/projects/${project.id}/edit`}>
<a>Edit</a>
</Link>
<button
type="button"
onClick={async () => {
if (window.confirm("This will be deleted")) {
await deleteProjectMutation({id: project.id})
router.push("/projects")
}
}}
style={{marginLeft: "0.5rem"}}
>
Delete
</button>
</div>
</>
)
}
const ShowProjectPage: BlitzPage = () => {
return (
<div>
<p>
<Link href="/projects">
<a>Projects</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<Project />
</Suspense>
</div>
)
}
ShowProjectPage.authenticate = true
ShowProjectPage.getLayout = (page) => <Layout>{page}</Layout>
export default ShowProjectPage

View File

@@ -1,71 +0,0 @@
import {Suspense} from "react"
import {Head, Link, useRouter, useQuery, useMutation, useParam, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import getProject from "app/projects/queries/getProject"
import updateProject from "app/projects/mutations/updateProject"
import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm"
export const EditProject = () => {
const router = useRouter()
const projectId = useParam("projectId", "number")
const [project, {setQueryData}] = useQuery(getProject, {id: projectId})
const [updateProjectMutation] = useMutation(updateProject)
return (
<>
<Head>
<title>Edit Project {project.id}</title>
</Head>
<div>
<h1>Edit Project {project.id}</h1>
<pre>{JSON.stringify(project)}</pre>
<ProjectForm
submitText="Update Project"
// TODO use a zod schema for form validation
// - Tip: extract mutation's schema into a shared `validations.ts` file and
// then import and use it here
// schema={UpdateProject}
initialValues={project}
onSubmit={async (values) => {
try {
const updated = await updateProjectMutation({
id: project.id,
...values,
})
await setQueryData(updated)
router.push(`/projects/${updated.id}`)
} catch (error) {
console.error(error)
return {
[FORM_ERROR]: error.toString(),
}
}
}}
/>
</div>
</>
)
}
const EditProjectPage: BlitzPage = () => {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<EditProject />
</Suspense>
<p>
<Link href="/projects">
<a>Projects</a>
</Link>
</p>
</div>
)
}
EditProjectPage.authenticate = true
EditProjectPage.getLayout = (page) => <Layout>{page}</Layout>
export default EditProjectPage

View File

@@ -1,67 +0,0 @@
import {Suspense} from "react"
import {Head, Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import getProjects from "app/projects/queries/getProjects"
const ITEMS_PER_PAGE = 100
export const ProjectsList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{projects, hasMore}] = usePaginatedQuery(getProjects, {
orderBy: {id: "asc"},
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<div>
<ul>
{projects.map((project) => (
<li key={project.id}>
<Link href={`/projects/${project.id}`}>
<a>{project.name}</a>
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
)
}
const ProjectsPage: BlitzPage = () => {
return (
<>
<Head>
<title>Projects</title>
</Head>
<div>
<p>
<Link href="/projects/new">
<a>Create Project</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<ProjectsList />
</Suspense>
</div>
</>
)
}
ProjectsPage.authenticate = {redirectTo: "/login"}
ProjectsPage.getLayout = (page) => <Layout>{page}</Layout>
export default ProjectsPage

View File

@@ -1,46 +0,0 @@
import {Link, useRouter, useMutation, BlitzPage} from "blitz"
import Layout from "app/core/layouts/Layout"
import createProject from "app/projects/mutations/createProject"
import {ProjectForm, FORM_ERROR} from "app/projects/components/ProjectForm"
const NewProjectPage: BlitzPage = () => {
const router = useRouter()
const [createProjectMutation] = useMutation(createProject)
return (
<div>
<h1>Create New Project</h1>
<ProjectForm
submitText="Create Project"
// TODO use a zod schema for form validation
// - Tip: extract mutation's schema into a shared `validations.ts` file and
// then import and use it here
// schema={CreateProject}
// initialValues={{}}
onSubmit={async (values) => {
try {
const project = await createProjectMutation(values)
router.push(`/projects/${project.id}`)
} catch (error) {
console.error(error)
return {
[FORM_ERROR]: error.toString(),
}
}
}}
/>
<p>
<Link href="/projects">
<a>Projects</a>
</Link>
</p>
</div>
)
}
NewProjectPage.authenticate = true
NewProjectPage.getLayout = (page) => <Layout title={"Create New Project"}>{page}</Layout>
export default NewProjectPage

View File

@@ -1,28 +1,36 @@
import {FC} from "react"
import {getSessionContext} from "@blitzjs/server"
import {
getSession,
invokeWithMiddleware,
useRouter,
GetServerSideProps,
PromiseReturnType,
ErrorComponent as ErrorPage,
useMutation,
AuthenticationError,
AuthorizationError,
GetServerSideProps,
InferGetServerSidePropsType,
BlitzPage,
} from "blitz"
import getUser from "app/users/queries/getUser"
import logout from "app/auth/mutations/logout"
import path from "path"
export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
type PageProps = {
user?: PromiseReturnType<typeof getUser>
error?: {
statusCode: number
message: string
}
}
export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, res}) => {
// Ensure these files are not eliminated by trace-based tree-shaking (like Vercel)
// https://github.com/blitz-js/blitz/issues/794
path.resolve("next.config.js")
path.resolve("blitz.config.js")
path.resolve(".next/blitz/db.js")
path.resolve(".next/__db.js")
// End anti-tree-shaking
const session = await getSession(req, res)
const session = await getSessionContext(req, res)
console.log("Session id:", session.userId)
try {
const user = await invokeWithMiddleware(
@@ -54,7 +62,7 @@ export const getServerSideProps: GetServerSideProps = async ({req, res}) => {
}
}
const Test: BlitzPage<InferGetServerSidePropsType<typeof getServerSideProps>> = ({user, error}) => {
const Test: FC<PageProps> = ({user, error}: PageProps) => {
const router = useRouter()
const [logoutMutation] = useMutation(logout)

View File

@@ -1,12 +0,0 @@
import {Form, FormProps} from "app/core/components/Form"
import {LabeledTextField} from "app/core/components/LabeledTextField"
import * as z from "zod"
export {FORM_ERROR} from "app/core/components/Form"
export function ProjectForm<S extends z.ZodType<any, any>>(props: FormProps<S>) {
return (
<Form<S> {...props}>
<LabeledTextField name="name" label="Name" placeholder="Name" />
</Form>
)
}

View File

@@ -1,16 +0,0 @@
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
const CreateProject = z
.object({
name: z.string(),
})
.nonstrict()
export default resolver.pipe(resolver.zod(CreateProject), resolver.authorize(), async (input) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.create({data: input})
return project
})

View File

@@ -1,16 +0,0 @@
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
const DeleteProject = z
.object({
id: z.number(),
})
.nonstrict()
export default resolver.pipe(resolver.zod(DeleteProject), resolver.authorize(), async ({id}) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.delete({where: {id}})
return project
})

View File

@@ -1,21 +0,0 @@
import {resolver} from "blitz"
import db from "db"
import * as z from "zod"
const UpdateProject = z
.object({
id: z.number(),
name: z.string(),
})
.nonstrict()
export default resolver.pipe(
resolver.zod(UpdateProject),
resolver.authorize(),
async ({id, ...data}) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.update({where: {id}, data})
return project
},
)

View File

@@ -1,17 +0,0 @@
import {resolver, NotFoundError} from "blitz"
import db from "db"
import * as z from "zod"
const GetProject = z.object({
// This accepts type of undefined, but is required at runtime
id: z.number().optional().refine(Boolean, "Required"),
})
export default resolver.pipe(resolver.zod(GetProject), resolver.authorize(), async ({id}) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const project = await db.project.findFirst({where: {id}})
if (!project) throw new NotFoundError()
return project
})

View File

@@ -1,25 +0,0 @@
import {paginate, resolver} from "blitz"
import db, {Prisma} from "db"
interface GetProjectsInput
extends Pick<Prisma.ProjectFindManyArgs, "where" | "orderBy" | "skip" | "take"> {}
export default resolver.pipe(
resolver.authorize(),
async ({where, orderBy, skip = 0, take = 100}: GetProjectsInput) => {
// TODO: in multi-tenant app, you must add validation to ensure correct tenant
const {items: projects, hasMore, nextPage, count} = await paginate({
skip,
take,
count: () => db.project.count({where}),
query: (paginateArgs) => db.project.findMany({...paginateArgs, where, orderBy}),
})
return {
projects,
nextPage,
hasMore,
count,
}
},
)

View File

@@ -1,9 +1,9 @@
import {Ctx} from "blitz"
export default async function trackView(_: any, {session}: Ctx) {
const currentViews = session.views || 0
await session.$setPublicData({views: currentViews + 1})
await session.$setPrivateData({views: currentViews + 1})
export default async function trackView(_ = null, {session}: Ctx) {
const currentViews = session.publicData.views || 0
await session.setPublicData({views: currentViews + 1})
await session.setPrivateData({views: currentViews + 1})
return
}

View File

@@ -0,0 +1,60 @@
import {Suspense} from "react"
import Layout from "app/layouts/Layout"
import {Link, usePaginatedQuery, useRouter, BlitzPage} from "blitz"
import getUsers from "app/users/queries/getUsers"
const ITEMS_PER_PAGE = 100
export const UsersList = () => {
const router = useRouter()
const page = Number(router.query.page) || 0
const [{users, hasMore}] = usePaginatedQuery(getUsers, {
orderBy: {id: "asc"},
skip: ITEMS_PER_PAGE * page,
take: ITEMS_PER_PAGE,
})
const goToPreviousPage = () => router.push({query: {page: page - 1}})
const goToNextPage = () => router.push({query: {page: page + 1}})
return (
<div>
<ul>
{users.map((user) => (
<li key={user.id}>
<Link href="/users/[userId]" as={`/users/${user.id}`}>
<a>{user.email}</a>
</Link>
</li>
))}
</ul>
<button disabled={page === 0} onClick={goToPreviousPage}>
Previous
</button>
<button disabled={!hasMore} onClick={goToNextPage}>
Next
</button>
</div>
)
}
const UsersPage: BlitzPage = () => {
return (
<div>
<p>
<Link href="/users/new">
<a>Create User</a>
</Link>
</p>
<Suspense fallback={<div>Loading...</div>}>
<UsersList />
</Suspense>
</div>
)
}
UsersPage.getLayout = (page) => <Layout>{page}</Layout>
export default UsersPage

View File

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

View File

@@ -2,15 +2,15 @@ import {Ctx, NotFoundError} from "blitz"
import db, {Prisma} from "db"
type GetUserInput = {
where: Prisma.UserFindFirstArgs["where"]
where: Prisma.FindUniqueUserArgs["where"]
}
export default async function getUser({where}: GetUserInput, ctx: Ctx) {
if (!ctx.session.userId) return null
ctx.session.authorize()
const user = await db.user.findFirst({where})
const user = await db().user.findFirst({where})
if (!user) throw new NotFoundError(`User with id ${where?.id} does not exist`)
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
const {hashedPassword, ...rest} = user

View File

@@ -1,19 +1,19 @@
import {Ctx} from "blitz"
import db, {Prisma} from "db"
import db, {FindManyUserArgs} from "db"
type GetUsersInput = Pick<Prisma.UserFindManyArgs, "where" | "orderBy" | "skip" | "take">
type GetUsersInput = Pick<FindManyUserArgs, "where" | "orderBy" | "skip" | "take">
export default async function getUsers({where, orderBy, skip = 0, take}: GetUsersInput, ctx: Ctx) {
ctx.session.$authorize()
ctx.session.authorize()
const users = await db.user.findMany({
const users = await db().user.findMany({
where,
orderBy,
take,
skip,
})
const count = await db.user.count()
const count = await db().user.count()
const hasMore = typeof take === "number" ? skip + take < count : false
const nextPage = hasMore ? {take, skip: skip + take!} : null

View File

@@ -1,24 +1,22 @@
const {sessionMiddleware, simpleRolesIsAuthorized} = require("blitz")
const withMonorepoBuildTooling = require("@preconstruct/next")
const {sessionMiddleware, simpleRolesIsAuthorized} = require("@blitzjs/server")
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true",
})
module.exports = withMonorepoBuildTooling(
withBundleAnalyzer({
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
// sessionExpiryMinutes: 4,
}),
],
log: {
// level: "trace",
},
experimental: {
isomorphicResolverImports: false,
},
/*
module.exports = withBundleAnalyzer({
middleware: [
sessionMiddleware({
isAuthorized: simpleRolesIsAuthorized,
sessionExpiryMinutes: 4,
}),
],
log: {
level: "trace",
},
experimental: {
isomorphicResolverImports: true,
},
/*
webpack: (config, {buildId, dev, isServer, defaultLoaders, webpack}) => {
// Note: we provide webpack above so you should not `require` it
// Perform customizations to webpack config
@@ -31,5 +29,4 @@ module.exports = withMonorepoBuildTooling(
return config
},
*/
}),
)
})

View File

@@ -19,7 +19,6 @@ describe("index page", () => {
const user = createRandomUser()
cy.signup(user)
cy.wait(1000)
cy.location("pathname").should("equal", "/")
cy.contains("button", "Logout")
@@ -29,10 +28,8 @@ describe("index page", () => {
const user = createRandomUser()
cy.signup(user)
cy.wait(1000)
cy.contains("button", "Logout").click()
cy.wait(1000)
cy.contains("a", /login/i).click()
cy.contains("Email").find("input").type(user.email)
@@ -40,7 +37,6 @@ describe("index page", () => {
cy.contains("button", /login/i).click()
cy.location("pathname").should("equal", "/")
cy.wait(1000)
cy.contains("button", "Logout")
})
@@ -48,12 +44,10 @@ describe("index page", () => {
const user = createRandomUser()
cy.signup(user)
cy.wait(1000)
cy.contains("button", "Logout").click()
cy.location("pathname").should("equal", "/")
cy.wait(1000)
cy.contains("a", /login/i)
})
@@ -63,13 +57,10 @@ describe("index page", () => {
const user = createRandomUser()
cy.contains("button", "Track view").click()
cy.wait(500)
cy.contains("button", "Track view").click()
cy.wait(1000)
cy.contains('"views": 2')
cy.signup(user)
cy.wait(1000)
cy.contains('"views": 2')
})

View File

@@ -1,7 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "es5"],
"types": ["cypress"]
}
}

View File

@@ -1,7 +1,7 @@
import {enhancePrisma} from "blitz"
import {PrismaClient} from "@prisma/client"
const EnhancedPrisma = enhancePrisma(PrismaClient)
export * from "@prisma/client"
export default new EnhancedPrisma()
export default function db(): PrismaClient {
globalThis.prisma = globalThis.prisma || new PrismaClient()
return globalThis.prisma
}

View File

@@ -0,0 +1,82 @@
# Migration `20200704211726`
This migration has been generated by Brandon Bayer at 7/4/2020, 9:17:26 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
PRAGMA foreign_keys=OFF;
CREATE TABLE "quaint"."User" (
"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP ,"email" TEXT NOT NULL ,"hashedPassword" TEXT NOT NULL ,"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"name" TEXT ,"role" TEXT NOT NULL ,"updatedAt" DATE NOT NULL )
CREATE TABLE "quaint"."Session" (
"antiCSRFToken" TEXT ,"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP ,"expiresAt" DATE ,"handle" TEXT NOT NULL ,"hashedSessionToken" TEXT ,"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"privateData" TEXT ,"publicData" TEXT ,"updatedAt" DATE NOT NULL ,"userId" INTEGER ,FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE)
CREATE UNIQUE INDEX "quaint"."User.email" ON "User"("email")
CREATE UNIQUE INDEX "quaint"."Session.handle" ON "Session"("handle")
PRAGMA "quaint".foreign_key_check;
PRAGMA foreign_keys=ON;
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration ..20200704211726
--- datamodel.dml
+++ datamodel.dml
@@ -1,0 +1,46 @@
+// This is your Prisma schema file,
+// learn more about it in the docs: https://pris.ly/d/prisma-schema
+
+datasource sqlite {
+ provider = "sqlite"
+ url = "***"
+}
+
+// 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 = "***"
+//}
+
+generator client {
+ provider = "prisma-client-js"
+}
+
+
+// --------------------------------------
+
+model User {
+ id Int @default(autoincrement()) @id
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ name String?
+ email String @unique
+ hashedPassword String
+ role String
+ sessions Session[]
+}
+
+model Session {
+ id Int @default(autoincrement()) @id
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+ expiresAt DateTime?
+ handle String @unique
+ user User? @relation(fields: [userId], references: [id])
+ userId Int?
+ hashedSessionToken String?
+ antiCSRFToken String?
+ publicData String?
+ privateData String?
+}
```

View File

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

View File

@@ -0,0 +1,373 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "CreateSource",
"source": "sqlite"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "sqlite"
},
"argument": "provider",
"value": "\"sqlite\""
},
{
"tag": "CreateArgument",
"location": {
"tag": "Source",
"source": "sqlite"
},
"argument": "url",
"value": "\"file:./db.sqlite\""
},
{
"tag": "CreateModel",
"model": "User"
},
{
"tag": "CreateField",
"model": "User",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "createdAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "createdAt"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "createdAt"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "User",
"field": "updatedAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "updatedAt"
},
"directive": "updatedAt"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "name",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "User",
"field": "email",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "email"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "User",
"field": "hashedPassword",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "User",
"field": "role",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateField",
"model": "User",
"field": "sessions",
"type": "Session",
"arity": "List"
},
{
"tag": "CreateModel",
"model": "Session"
},
{
"tag": "CreateField",
"model": "Session",
"field": "id",
"type": "Int",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "id"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Session",
"field": "id"
},
"directive": "default"
},
"argument": "",
"value": "autoincrement()"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "id"
},
"directive": "id"
}
},
{
"tag": "CreateField",
"model": "Session",
"field": "createdAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "createdAt"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Session",
"field": "createdAt"
},
"directive": "default"
},
"argument": "",
"value": "now()"
},
{
"tag": "CreateField",
"model": "Session",
"field": "updatedAt",
"type": "DateTime",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "updatedAt"
},
"directive": "updatedAt"
}
},
{
"tag": "CreateField",
"model": "Session",
"field": "expiresAt",
"type": "DateTime",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "Session",
"field": "handle",
"type": "String",
"arity": "Required"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "handle"
},
"directive": "unique"
}
},
{
"tag": "CreateField",
"model": "Session",
"field": "user",
"type": "User",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "Session",
"field": "user"
},
"directive": "relation"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Session",
"field": "user"
},
"directive": "relation"
},
"argument": "fields",
"value": "[userId]"
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "Session",
"field": "user"
},
"directive": "relation"
},
"argument": "references",
"value": "[id]"
},
{
"tag": "CreateField",
"model": "Session",
"field": "userId",
"type": "Int",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "Session",
"field": "hashedSessionToken",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "Session",
"field": "antiCSRFToken",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "Session",
"field": "publicData",
"type": "String",
"arity": "Optional"
},
{
"tag": "CreateField",
"model": "Session",
"field": "privateData",
"type": "String",
"arity": "Optional"
}
]
}

View File

@@ -0,0 +1,66 @@
# Migration `20200711145940`
This migration has been generated by Brandon Bayer at 7/11/2020, 2:59:40 PM.
You can check out the [state of the schema](./schema.prisma) after the migration.
## Database Steps
```sql
PRAGMA foreign_keys=OFF;
CREATE TABLE "quaint"."new_User" (
"createdAt" DATE NOT NULL DEFAULT CURRENT_TIMESTAMP ,"email" TEXT NOT NULL ,"hashedPassword" TEXT ,"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,"name" TEXT ,"role" TEXT NOT NULL DEFAULT 'user' ,"updatedAt" DATE NOT NULL )
INSERT INTO "quaint"."new_User" ("createdAt", "email", "hashedPassword", "id", "name", "role", "updatedAt") SELECT "createdAt", "email", "hashedPassword", "id", "name", "role", "updatedAt" FROM "quaint"."User"
PRAGMA foreign_keys=off;
DROP TABLE "quaint"."User";;
PRAGMA foreign_keys=on
ALTER TABLE "quaint"."new_User" RENAME TO "User";
CREATE UNIQUE INDEX "quaint"."User.email" ON "User"("email")
PRAGMA "quaint".foreign_key_check;
PRAGMA foreign_keys=ON;
```
## Changes
```diff
diff --git schema.prisma schema.prisma
migration 20200704211726..20200711145940
--- datamodel.dml
+++ datamodel.dml
@@ -2,16 +2,16 @@
// learn more about it in the docs: https://pris.ly/d/prisma-schema
datasource sqlite {
provider = "sqlite"
- url = "***"
+ url = "***"
}
// 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 = "***"
+// url = "***"
//}
generator client {
provider = "prisma-client-js"
@@ -25,10 +25,10 @@
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String @unique
- hashedPassword String
- role String
+ hashedPassword String?
+ role String @default("user")
sessions Session[]
}
model Session {
```

View File

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

View File

@@ -0,0 +1,36 @@
{
"version": "0.3.14-fixed",
"steps": [
{
"tag": "UpdateField",
"model": "User",
"field": "hashedPassword",
"arity": "Optional"
},
{
"tag": "CreateDirective",
"location": {
"path": {
"tag": "Field",
"model": "User",
"field": "role"
},
"directive": "default"
}
},
{
"tag": "CreateArgument",
"location": {
"tag": "Directive",
"path": {
"tag": "Field",
"model": "User",
"field": "role"
},
"directive": "default"
},
"argument": "",
"value": "\"user\""
}
]
}

View File

@@ -1,32 +0,0 @@
-- 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,
"userId" INTEGER,
"hashedSessionToken" TEXT,
"antiCSRFToken" TEXT,
"publicData" TEXT,
"privateData" TEXT,
FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "User.email_unique" ON "User"("email");
-- CreateIndex
CREATE UNIQUE INDEX "Session.handle_unique" ON "Session"("handle");

View File

@@ -1,7 +0,0 @@
-- CreateTable
CREATE TABLE "Project" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"name" TEXT NOT NULL
);

View File

@@ -1,2 +0,0 @@
-- AlterTable
ALTER TABLE "Project" ADD COLUMN "dueDate" DATETIME;

View File

@@ -1,15 +0,0 @@
-- 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,
FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
);
-- CreateIndex
CREATE UNIQUE INDEX "Token.hashedToken_type_unique" ON "Token"("hashedToken", "type");

View File

@@ -0,0 +1,4 @@
# Prisma Migrate lockfile v1
20200704211726
20200711145940

View File

@@ -1,2 +0,0 @@
# Please do not edit this file manually
provider = "sqlite"

View File

@@ -20,20 +20,18 @@ generator client {
// --------------------------------------
model User {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String?
email String @unique
email String @unique
hashedPassword String?
role String @default("user")
sessions Session[]
tokens Token[]
role String @default("user")
sessions Session[]
}
model Session {
id Int @id @default(autoincrement())
id Int @default(autoincrement()) @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
expiresAt DateTime?
@@ -45,26 +43,3 @@ model Session {
publicData String?
privateData String?
}
model Token {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
hashedToken String
type String
expiresAt DateTime
sentTo String
user User @relation(fields: [userId], references: [id])
userId Int
@@unique([hashedToken, type])
}
model Project {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String
dueDate DateTime?
}

View File

@@ -1,6 +0,0 @@
import db from "./index"
const seed = async () => {
const user = await db.user.create({data: {name: "FooBar", email: "hey@" + new Date().getTime()}})
console.log("Created user", user)
}
export default seed

View File

@@ -1,3 +1,30 @@
const {pathsToModuleNameMapper} = require("ts-jest/utils")
const {compilerOptions} = require("./tsconfig")
module.exports = {
preset: "blitz",
// Test setup file
setupFilesAfterEnv: ["<rootDir>/test/setup.ts"],
// Add type checking to Typescript test files
preset: "ts-jest",
testEnvironment: "jest-environment-jsdom-fourteen",
// Automatically clear mock calls and instances between every test
clearMocks: true,
testPathIgnorePatterns: ["/node_modules/", "/.blitz/", "/.next/", "<rootDir>/db/migrations"],
transformIgnorePatterns: ["[/\\\\]node_modules[/\\\\].+\\.(ts|tsx)$"],
transform: {
"^.+\\.(ts|tsx)$": "babel-jest",
},
// This makes absolute imports work
moduleDirectories: ["node_modules", "<rootDir>"],
modulePathIgnorePatterns: ["<rootDir>/.blitz", "<rootDir>/.next", "<rootDir>/cypress"],
moduleNameMapper: {
// This ensures any path aliases in tsconfig also work in jest
...pathsToModuleNameMapper(compilerOptions.paths || {}),
"\\.(css|less|sass|scss)$": "identity-obj-proxy",
"\\.(gif|ttf|eot|svg|png|jpg|jpeg)$": "<rootDir>/test/__mocks__/fileMock.js",
},
watchPlugins: ["jest-watch-typeahead/filename", "jest-watch-typeahead/testname"],
// Coverage output
coverageDirectory: ".coverage",
collectCoverageFrom: ["**/*.{js,jsx,ts,tsx}", "!**/*.d.ts", "!**/node_modules/**"],
}

View File

@@ -1,45 +0,0 @@
/* TODO - You need to add a mailer integration in `integrations/` and import here.
*
* The integration file can be very simple. Instantiate the email client
* and then export it. That way you can import here and anywhere else
* and use it straight away.
*/
import previewEmail from "preview-email"
type ResetPasswordMailer = {
to: string
token: string
}
export function forgotPasswordMailer({to, token}: ResetPasswordMailer) {
// In production, set APP_ORIGIN to your production server origin
const origin = process.env.APP_ORIGIN || process.env.BLITZ_DEV_SERVER_ORIGIN
const resetUrl = `${origin}/reset-password?token=${token}`
const msg = {
from: "TODO@example.com",
to,
subject: "Your Password Reset Instructions",
html: `
<h1>Reset Your Password</h1>
<h3>NOTE: You must set up a production email integration in mailers/forgotPasswordMailer.ts</h3>
<a href="${resetUrl}">
Click here to set a new password
</a>
`,
}
return {
async send() {
if (process.env.NODE_ENV === "production") {
// TODO - send the production email, like this:
// await postmark.sendEmail(msg)
throw new Error("No production email implementation in mailers/forgotPasswordMailer")
} else {
// Preview email in the browser
await previewEmail(msg)
}
},
}
}

View File

@@ -1,18 +1,17 @@
{
"name": "@examples/auth",
"version": "0.32.1",
"version": "0.28.0-canary.1",
"scripts": {
"dev": "blitz dev",
"build": "blitz build",
"start": "blitz start",
"studio": "blitz prisma studio",
"studio": "blitz db studio",
"build": "blitz build",
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
"analyze": "cross-env ANALYZE=true blitz build",
"cy:open": "cypress open",
"cy:run": "cypress run --browser chrome",
"cy:run": "cypress run",
"test": "prisma generate && yarn test:jest && yarn test:e2e",
"test:jest": "jest",
"test:server": "cross-env NODE_ENV=test blitz prisma migrate deploy --preview-feature && blitz build && cross-env NODE_ENV=test blitz start -p 3099",
"test:server": "blitz db migrate && blitz build && blitz start --production -p 3099",
"test:e2e": "cross-env NODE_ENV=test start-server-and-test test:server http://localhost:3099 cy:run"
},
"browserslist": [
@@ -27,40 +26,67 @@
"bracketSpacing": false,
"trailingComma": "all"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged && pretty-quick --staged",
"pre-push": "blitz test"
}
},
"lint-staged": {
"*.{js,ts,tsx}": [
"eslint --fix"
]
},
"dependencies": {
"@prisma/client": "2.17.0",
"blitz": "0.32.1",
"@prisma/cli": "2.12.0",
"@prisma/client": "2.12.0",
"blitz": "0.28.0-canary.1",
"final-form": "4.20.1",
"passport-auth0": "1.4.0",
"passport-github2": "0.1.12",
"passport-twitter": "1.0.4",
"prisma": "2.17.0",
"react": "0.0.0-experimental-3310209d0",
"react-dom": "0.0.0-experimental-3310209d0",
"react-error-boundary": "3.1.1",
"react-error-boundary": "3.1.0",
"react-final-form": "6.5.2",
"secure-password": "4.0.0",
"zod": "1.11.11"
},
"devDependencies": {
"@cypress/skip-test": "2.6.0",
"@next/bundle-analyzer": "^10.0.6",
"@testing-library/react": "11.2.5",
"@testing-library/react-hooks": "^4.0.1",
"@cypress/skip-test": "2.5.1",
"@next/bundle-analyzer": "latest",
"@testing-library/jest-dom": "5.11.8",
"@testing-library/react": "11.2.2",
"@testing-library/react-hooks": "3.7.0",
"@types/jest": "26.0.19",
"@types/passport-auth0": "1.0.4",
"@types/passport-github2": "1.2.4",
"@types/passport-twitter": "1.0.36",
"@types/preview-email": "2.0.0",
"@types/react": "17.0.2",
"@types/react": "17.0.0",
"@types/secure-password": "3.1.0",
"@typescript-eslint/eslint-plugin": "4.11.1",
"@typescript-eslint/parser": "4.11.1",
"babel-eslint": "10.1.0",
"cross-env": "7.0.3",
"cypress": "6.2.1",
"eslint": "7.21.0",
"husky": "5.1.2",
"lint-staged": "10.5.4",
"cypress": "6.2.0",
"eslint": "7.16.0",
"eslint-config-react-app": "6.0.0",
"eslint-plugin-cypress": "2.11.2",
"eslint-plugin-flowtype": "5.2.0",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-react": "7.22.0",
"eslint-plugin-react-hooks": "4.2.0",
"husky": "4.3.6",
"jest": "26.6.3",
"jest-environment-jsdom-fourteen": "1.0.1",
"jest-watch-typeahead": "0.6.1",
"lint-staged": "10.5.3",
"prettier": "2.2.1",
"pretty-quick": "3.1.0",
"preview-email": "3.0.3",
"start-server-and-test": "1.11.7",
"typescript": "4.1.5"
"ts-jest": "26.4.4",
"typescript": "4.1.3"
},
"private": true
}

View File

@@ -1,4 +1,6 @@
// This is the jest 'setupFilesAfterEnv' setup file
// It's a good place to set globals, add global before/after hooks, etc
export {} // so TS doesn't complain
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom/extend-expect"
require("dotenv-flow").config({silent: true})

View File

@@ -13,6 +13,14 @@ export * from "@testing-library/react"
// This is the place to add any other context providers you need while testing.
// --------------------------------------------------------------------------------
type DefaultParams = Parameters<typeof defaultRender>
type RenderUI = DefaultParams[0]
type RenderOptions = DefaultParams[1] & {router?: Partial<BlitzRouter>}
type DefaultHookParams = Parameters<typeof defaultRenderHook>
type RenderHook = DefaultHookParams[0]
type RenderHookOptions = DefaultHookParams[1] & {router?: Partial<BlitzRouter>}
// --------------------------------------------------
// render()
// --------------------------------------------------
@@ -65,9 +73,6 @@ export const mockRouter: BlitzRouter = {
asPath: "/",
params: {},
query: {},
isReady: true,
isLocaleDomain: false,
isPreview: false,
push: jest.fn(),
replace: jest.fn(),
reload: jest.fn(),
@@ -81,11 +86,3 @@ export const mockRouter: BlitzRouter = {
},
isFallback: false,
}
type DefaultParams = Parameters<typeof defaultRender>
type RenderUI = DefaultParams[0]
type RenderOptions = DefaultParams[1] & {router?: Partial<BlitzRouter>}
type DefaultHookParams = Parameters<typeof defaultRenderHook>
type RenderHook = DefaultHookParams[0]
type RenderHookOptions = DefaultHookParams[1] & {router?: Partial<BlitzRouter>}

View File

@@ -1,11 +1,11 @@
{
"compilerOptions": {
"strict": true,
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"baseUrl": "./",
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
@@ -14,9 +14,8 @@
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"downlevelIteration": true,
"jsx": "preserve"
},
"exclude": ["node_modules", "cypress"],
"include": ["**/*.ts", "**/*.tsx"]
"exclude": ["node_modules"],
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
}

View File

@@ -1 +0,0 @@
../../types

View File

@@ -1,18 +1,12 @@
import {DefaultCtx, SessionContext, SimpleRolesIsAuthorized} from "blitz"
import {DefaultCtx, SessionContext, DefaultPublicData} from "blitz"
import {User} from "db"
export type Role = "ADMIN" | "USER"
declare module "blitz" {
export interface Ctx extends DefaultCtx {
session: SessionContext
}
export interface Session {
isAuthorized: SimpleRolesIsAuthorized<Role>
PublicData: {
userId: User["id"]
role: Role
views?: number
}
export interface PublicData extends DefaultPublicData {
userId: User["id"]
views?: number
}
}

View File

@@ -1,2 +0,0 @@
# This env file should be checked into source control
# This is the place for default values that should be used in all environments

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