Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
767926b34a |
@@ -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,
|
||||
|
||||
@@ -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
|
||||
55
.eslintrc.js
55
.eslintrc.js
@@ -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
23
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal 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.
|
||||
49
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
49
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -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:"
|
||||
5
.github/ISSUE_TEMPLATE/config.yml
vendored
5
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -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
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
2
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -2,7 +2,7 @@
|
||||
name: Feature/change request
|
||||
about: Something new or better!
|
||||
title: ""
|
||||
labels: "status/triage"
|
||||
labels: ""
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
|
||||
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
2
.github/PULL_REQUEST_TEMPLATE.md
vendored
@@ -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 -->
|
||||
|
||||
67
.github/workflows/codeql-analysis.yml
vendored
67
.github/workflows/codeql-analysis.yml
vendored
@@ -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
|
||||
11
.github/workflows/main.yml
vendored
11
.github/workflows/main.yml
vendored
@@ -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
|
||||
|
||||
270
.github/workflows/nextjs.yml
vendored
270
.github/workflows/nextjs.yml
vendored
@@ -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
6
.gitignore
vendored
@@ -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
1
.husky/.gitignore
vendored
@@ -1 +0,0 @@
|
||||
_
|
||||
@@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
yarn lint-staged
|
||||
yarn pretty-quick --staged
|
||||
@@ -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
11
GOVERNANCE.md
Normal 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.
|
||||
2
LICENSE
2
LICENSE
@@ -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
1
MAINTAINERS.md
Normal file
@@ -0,0 +1 @@
|
||||
This document has moved here: https://blitzjs.com/docs/maintainers
|
||||
3
MANIFESTO.md
Normal file
3
MANIFESTO.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Manifesto
|
||||
|
||||
[The Manifesto has been moved to Blitzjs.com](https://blitzjs.com/docs/manifesto)
|
||||
148
README.md
148
README.md
@@ -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>
|
||||
|
||||

|
||||
|
||||
<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>
|
||||
|
||||
|
||||
10
SECURITY.md
10
SECURITY.md
@@ -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.
|
||||
|
||||
@@ -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 |
@@ -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"],
|
||||
},
|
||||
],
|
||||
],
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
module.exports = {
|
||||
presets: ["blitz/babel"],
|
||||
presets: ["next/babel"],
|
||||
plugins: [],
|
||||
}
|
||||
@@ -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
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) => ({
|
||||
),
|
||||
},
|
||||
],
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
@@ -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 > * + * {
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
23
examples/auth/app/layouts/Layout.tsx
Normal file
23
examples/auth/app/layouts/Layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
})
|
||||
@@ -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
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
60
examples/auth/app/users/pages/users/index.tsx
Normal file
60
examples/auth/app/users/pages/users/index.tsx
Normal 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
|
||||
@@ -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},
|
||||
})
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
},
|
||||
*/
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "es5"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
82
examples/auth/db/migrations/20200704211726/README.md
Normal file
82
examples/auth/db/migrations/20200704211726/README.md
Normal 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?
|
||||
+}
|
||||
```
|
||||
|
||||
|
||||
46
examples/auth/db/migrations/20200704211726/schema.prisma
Normal file
46
examples/auth/db/migrations/20200704211726/schema.prisma
Normal 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?
|
||||
}
|
||||
373
examples/auth/db/migrations/20200704211726/steps.json
Normal file
373
examples/auth/db/migrations/20200704211726/steps.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
66
examples/auth/db/migrations/20200711145940/README.md
Normal file
66
examples/auth/db/migrations/20200711145940/README.md
Normal 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 {
|
||||
```
|
||||
|
||||
|
||||
46
examples/auth/db/migrations/20200711145940/schema.prisma
Normal file
46
examples/auth/db/migrations/20200711145940/schema.prisma
Normal 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?
|
||||
}
|
||||
36
examples/auth/db/migrations/20200711145940/steps.json
Normal file
36
examples/auth/db/migrations/20200711145940/steps.json
Normal 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\""
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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");
|
||||
@@ -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
|
||||
);
|
||||
@@ -1,2 +0,0 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Project" ADD COLUMN "dueDate" DATETIME;
|
||||
@@ -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");
|
||||
4
examples/auth/db/migrations/migrate.lock
Normal file
4
examples/auth/db/migrations/migrate.lock
Normal file
@@ -0,0 +1,4 @@
|
||||
# Prisma Migrate lockfile v1
|
||||
|
||||
20200704211726
|
||||
20200711145940
|
||||
@@ -1,2 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
provider = "sqlite"
|
||||
@@ -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?
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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/**"],
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../types
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user