Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5ead166ac | ||
|
|
2b07a8b119 |
@@ -609,8 +609,7 @@
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1329874?v=4",
|
||||
"profile": "https://jamiedavenport.dev",
|
||||
"contributions": [
|
||||
"code",
|
||||
"maintenance"
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -994,8 +993,7 @@
|
||||
"contributions": [
|
||||
"code",
|
||||
"maintenance",
|
||||
"question",
|
||||
"doc"
|
||||
"question"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1004,8 +1002,7 @@
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1430136?v=4",
|
||||
"profile": "https://github.com/sirmyron",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1015,8 +1012,7 @@
|
||||
"profile": "https://github.com/engelkes-finstreet",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code",
|
||||
"maintenance"
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -1266,593 +1262,6 @@
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sandulat",
|
||||
"name": "Stratulat Alexandru",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/7345874?v=4",
|
||||
"profile": "https://sandulat.com/",
|
||||
"contributions": [
|
||||
"code",
|
||||
"maintenance"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aericson",
|
||||
"name": "André Ericson",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/692542?v=4",
|
||||
"profile": "https://github.com/aericson",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cajotafer",
|
||||
"name": "Carlos Fernández",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/41461969?v=4",
|
||||
"profile": "http://Cajotafer.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Kosai106",
|
||||
"name": "Kevin Østerkilde",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/6379824?v=4",
|
||||
"profile": "https://oesterkilde.dk/",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "aaronfulkerson",
|
||||
"name": "aaronfulkerson",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/31112737?v=4",
|
||||
"profile": "https://github.com/aaronfulkerson",
|
||||
"contributions": [
|
||||
"code",
|
||||
"question"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "alexnaiman",
|
||||
"name": "Alexandru Naiman",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/25799714?v=4",
|
||||
"profile": "https://github.com/alexnaiman",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "davidlutta",
|
||||
"name": "David Ezekiel Lutta",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/14890315?v=4",
|
||||
"profile": "https://davidlutta.github.io/portfolio/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wanjuntham",
|
||||
"name": "wanjuntham",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/49380551?v=4",
|
||||
"profile": "https://github.com/wanjuntham",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nahue",
|
||||
"name": "Victor Nahuel Chaves",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/96837?v=4",
|
||||
"profile": "http://www.nahuelchaves.xyz",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "peter50216",
|
||||
"name": "Peter Shih",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/891109?v=4",
|
||||
"profile": "https://github.com/peter50216",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sewerynkalemba",
|
||||
"name": "Seweryn Kalemba",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/37031328?v=4",
|
||||
"profile": "http://seweryn.kale.mba",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nksaraf",
|
||||
"name": "Nikhil Saraf",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/11255148?v=4",
|
||||
"profile": "https://nksaraf.github.io",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "zanedb",
|
||||
"name": "Zane",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/16865690?v=4",
|
||||
"profile": "https://zane.sh",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dulcehc",
|
||||
"name": "Dulce Hernández",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/19391835?v=4",
|
||||
"profile": "https://github.com/dulcehc",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "markhaehnel",
|
||||
"name": "Mark Hähnel",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/1516205?v=4",
|
||||
"profile": "https://markhaehnel.de",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "nemesv",
|
||||
"name": "Viktor Nemes",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/251330?v=4",
|
||||
"profile": "http://stackoverflow.com/users/872395/nemesv",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "goleary",
|
||||
"name": "Gabe O'Leary",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/16123225?v=4",
|
||||
"profile": "http://gabeoleary.com",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "machadolucasvp",
|
||||
"name": "Lucas Machado",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/44952113?v=4",
|
||||
"profile": "https://github.com/machadolucasvp",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "maciekgrzybek",
|
||||
"name": "maciek_grzybek",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/16546428?v=4",
|
||||
"profile": "https://github.com/maciekgrzybek",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mweibel",
|
||||
"name": "Michael Weibel",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/307427?v=4",
|
||||
"profile": "https://github.com/mweibel",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "isoppp",
|
||||
"name": "Hiroki Isogai",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/16318727?v=4",
|
||||
"profile": "https://isoppp.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "matamatanot",
|
||||
"name": "matamatanot",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/39780486?v=4",
|
||||
"profile": "https://github.com/matamatanot",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ericsakmar",
|
||||
"name": "Eric Sakmar",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/5620709?v=4",
|
||||
"profile": "https://github.com/ericsakmar",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "leggsimon",
|
||||
"name": "Simon Legg",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/11544418?v=4",
|
||||
"profile": "https://github.com/leggsimon",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "wobsoriano",
|
||||
"name": "Robert Soriano",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/13049130?v=4",
|
||||
"profile": "https://robsoriano.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "benediktms",
|
||||
"name": "Benedikt Schnatterbeck",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/48836135?v=4",
|
||||
"profile": "https://github.com/benediktms",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Talor-A",
|
||||
"name": "Talor Anderson",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/11509865?v=4",
|
||||
"profile": "http://taloranderson.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "akirabaruah",
|
||||
"name": "Akira Baruah",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/6751517?v=4",
|
||||
"profile": "https://github.com/akirabaruah",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "cwray-tech",
|
||||
"name": "Christopher Wray",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/53663762?v=4",
|
||||
"profile": "https://chriswray.dev/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "piotrski",
|
||||
"name": "Piotrek Tomczewski",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/244174?v=4",
|
||||
"profile": "https://github.com/piotrski",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rap2hpoutre",
|
||||
"name": "Raphaël Huchet",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/1575946?v=4",
|
||||
"profile": "http://raph.site",
|
||||
"contributions": [
|
||||
"doc",
|
||||
"test",
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "KATT",
|
||||
"name": "Alex Johansson",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/459267?v=4",
|
||||
"profile": "http://kattcorp.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "dmzza",
|
||||
"name": "David Mazza",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/120893?v=4",
|
||||
"profile": "http://davidmazza.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "rayandrews",
|
||||
"name": "Ray Andrew",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/4437323?v=4",
|
||||
"profile": "https://github.com/rayandrews",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Mzaien",
|
||||
"name": "Abdullah Mzaien",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/43112535?v=4",
|
||||
"profile": "http://Dal.Design",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "williamkwao",
|
||||
"name": "William Kwao",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/8839514?v=4",
|
||||
"profile": "http://kwao.io",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "sakulstra",
|
||||
"name": "Lukas Strassel",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/4396533?v=4",
|
||||
"profile": "https://github.com/sakulstra",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tpatel",
|
||||
"name": "Thibaut Patel",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/494686?v=4",
|
||||
"profile": "https://thibpat.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jonstuebe",
|
||||
"name": "Jon Stuebe",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/156722?v=4",
|
||||
"profile": "http://jonstuebe.com",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "ugogo",
|
||||
"name": "Ugo Onali",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/5040476?v=4",
|
||||
"profile": "https://ugogo.dev",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "saintmalik",
|
||||
"name": "SaintMalik",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/37118134?v=4",
|
||||
"profile": "https://saintmalik.me",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "Khaledgarbaya",
|
||||
"name": "Khaled Garbaya",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/1156093?v=4",
|
||||
"profile": "https://khaledgarbaya.net",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "tundera",
|
||||
"name": "tundera",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/61833561?v=4",
|
||||
"profile": "https://tundera.dev",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "markylaing",
|
||||
"name": "markylaing",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/41469221?v=4",
|
||||
"profile": "https://github.com/markylaing",
|
||||
"contributions": [
|
||||
"code",
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "AkifumiSato",
|
||||
"name": "Akifumi Sato",
|
||||
"avatar_url": "https://avatars2.githubusercontent.com/u/25711332?v=4",
|
||||
"profile": "https://akfm.dev/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "beeplin",
|
||||
"name": "Beep LIN",
|
||||
"avatar_url": "https://avatars3.githubusercontent.com/u/13058150?v=4",
|
||||
"profile": "https://github.com/beeplin",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "mattfwood",
|
||||
"name": "Matt Wood",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/22530815?v=4",
|
||||
"profile": "https://mattwood.tech/",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "jackbravo",
|
||||
"name": "Joaquin Bravo Contreras",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/15214?v=4",
|
||||
"profile": "http://joaquin.axai.mx",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "arjundubey-cr",
|
||||
"name": "Arjun Dubey",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/40758425?v=4",
|
||||
"profile": "https://github.com/arjundubey-cr",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "chanand",
|
||||
"name": "chanand",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1317789?v=4",
|
||||
"profile": "https://github.com/chanand",
|
||||
"contributions": [
|
||||
"code"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "phillipkregg",
|
||||
"name": "phillipkregg",
|
||||
"avatar_url": "https://avatars0.githubusercontent.com/u/1066044?v=4",
|
||||
"profile": "https://github.com/phillipkregg",
|
||||
"contributions": [
|
||||
"doc"
|
||||
]
|
||||
},
|
||||
{
|
||||
"login": "timReynolds",
|
||||
"name": "Tim Reynolds",
|
||||
"avatar_url": "https://avatars1.githubusercontent.com/u/168870?v=4",
|
||||
"profile": "http://timothyreynolds.co.uk",
|
||||
"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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"contributorsPerLine": 7,
|
||||
|
||||
22
.eslintrc.js
22
.eslintrc.js
@@ -8,7 +8,7 @@ module.exports = {
|
||||
},
|
||||
project: `./tsconfig.json`,
|
||||
},
|
||||
plugins: ["@typescript-eslint", "import", "unicorn", "simple-import-sort"],
|
||||
plugins: ["@typescript-eslint", "import", "unicorn"],
|
||||
extends: ["react-app"],
|
||||
rules: {
|
||||
"react/react-in-jsx-scope": "off", // React is always in scope with Blitz
|
||||
@@ -30,26 +30,6 @@ module.exports = {
|
||||
// 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",
|
||||
{
|
||||
groups: [
|
||||
[
|
||||
// Side effect imports.
|
||||
"^\\u0000",
|
||||
// Packages.
|
||||
// Things that start with a letter (or digit or underscore), or `@` followed by a letter.
|
||||
"^@?\\w",
|
||||
// Absolute imports and other imports such as Vue-style `@/foo`.
|
||||
// Anything that does not start with a dot.
|
||||
"^[^.]",
|
||||
// Relative imports.
|
||||
// Anything that starts with a dot.
|
||||
"^\\.",
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
ignorePatterns: ["packages/cli/", "packages/generator/templates", ".eslintrc.js"],
|
||||
overrides: [
|
||||
|
||||
9
.github/CODEOWNERS
vendored
9
.github/CODEOWNERS
vendored
@@ -1,8 +1,11 @@
|
||||
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
|
||||
|
||||
* @flybayer
|
||||
*.md @merelinguist
|
||||
|
||||
packages/cli/**/* @aem, @flybayer
|
||||
packages/generator/**/* @aem @flybayer
|
||||
packages/server/**/* @ryardley
|
||||
packages/file-pipeline/**/* @ryardley
|
||||
packages/cli/**/* @aem
|
||||
packages/generator/**/* @aem
|
||||
packages/generator/templates**/* @flybayer
|
||||
packages/installer/**/* @aem @flybayer
|
||||
packages/installer/**/* @aem
|
||||
|
||||
7
.github/workflows/compressed.yml
vendored
7
.github/workflows/compressed.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Compressed Size
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -6,15 +6,14 @@ on:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Compressed Size
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node and Yarn
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "14"
|
||||
node-version: "12.x"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
yarn && yarn build
|
||||
|
||||
33
.github/workflows/main.yml
vendored
33
.github/workflows/main.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: CI
|
||||
name: Continuous Integration
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -17,9 +17,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: "14"
|
||||
node-version: "12.16.1"
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -32,9 +32,9 @@ jobs:
|
||||
**/node_modules
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v4-${{ hashFiles('yarn.lock') }}
|
||||
key: ${{ runner.os }}-yarn-v3-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.node_version}}-yarn-v4-
|
||||
${{ runner.os }}-yarn-v3-
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --silent
|
||||
env:
|
||||
@@ -44,18 +44,17 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
build_and_test_pkgs:
|
||||
name: Packages Tests
|
||||
name: Build & Test Packages
|
||||
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
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
node-version: "12.16.1"
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -67,9 +66,9 @@ jobs:
|
||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v4-${{ hashFiles('yarn.lock') }}
|
||||
key: ${{ runner.os }}-yarn-v2-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.node_version}}-yarn-v4-
|
||||
${{ runner.os }}-yarn-v2-
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --silent
|
||||
env:
|
||||
@@ -86,19 +85,17 @@ jobs:
|
||||
env:
|
||||
CI: true
|
||||
build_and_test_examples:
|
||||
timeout-minutes: 30
|
||||
name: Example Apps Tests
|
||||
name: Build & Test Examples
|
||||
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
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: ${{ matrix.node_version }}
|
||||
node-version: "12.16.1"
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "::set-output name=dir::$(yarn cache dir)"
|
||||
@@ -110,9 +107,9 @@ jobs:
|
||||
${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
/home/runner/.cache/Cypress
|
||||
C:\Users\runneradmin\AppData\Local\Cypress\Cache
|
||||
key: ${{ runner.os }}-${{ runner.node_version}}-yarn-v4-${{ hashFiles('yarn.lock') }}
|
||||
key: ${{ runner.os }}-yarn-v2-${{ hashFiles('yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ runner.node_version}}-yarn-v4-
|
||||
${{ runner.os }}-yarn-v2-
|
||||
- name: Install dependencies
|
||||
run: yarn install --frozen-lockfile --silent
|
||||
env:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -20,7 +20,3 @@ dist
|
||||
**/.envrc
|
||||
.blitz-*
|
||||
.blitz-cli-cache
|
||||
.vscode
|
||||
.tsbuildinfo
|
||||
.nvmrc
|
||||
**/.test*
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# .kodiak.toml
|
||||
# Minimal config. version is the only required field.
|
||||
version = 1
|
||||
merge.automerge_label = "0 - <(^_^)> - merge it! ✌️"
|
||||
approve.auto_approve_usernames = ["flybayer", "depfu"]
|
||||
@@ -1 +1 @@
|
||||
12.20.0
|
||||
12.16.1
|
||||
|
||||
135
README.md
135
README.md
@@ -6,7 +6,7 @@
|
||||
<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-196-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-132-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">
|
||||
@@ -40,9 +40,7 @@ You need Node.js 12 or newer
|
||||
|
||||
#### Install Blitz
|
||||
|
||||
Run `npm install -g blitz` or `yarn global add blitz`
|
||||
|
||||
_You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_
|
||||
Run `npm install -g blitz`
|
||||
|
||||
#### Create a New App
|
||||
|
||||
@@ -53,12 +51,6 @@ _You can alternatively use [`npx`](https://www.npmjs.com/package/npx)_
|
||||
|
||||
<br><br>
|
||||
|
||||
<a aria-label="Bytes Newsletter" href="https://ui.dev/bytes/?r=blitzjs">
|
||||
<img alt="Bytes Newsletter" src="https://files-8wtskjofb.vercel.app/smarter-16x1.jpg">
|
||||
</a>
|
||||
|
||||
<br><br>
|
||||
|
||||

|
||||
|
||||
<br><br>
|
||||
@@ -124,21 +116,16 @@ Your financial contributions help ensure Blitz continues to be developed and mai
|
||||
|
||||
👉 View options and contribute at [GitHub Sponsors](https://github.com/sponsors/blitz-js), [PayPal](https://paypal.me/thebayers), or [Open Collective](https://opencollective.com/blitzjs)
|
||||
|
||||
|
||||
### 🌱 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://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="30px"/>
|
||||
<img alt="" src="https://reactbricks.com/reactbricks_icon.svg" 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="110px">
|
||||
<a aria-label="Your Company" href="#">
|
||||
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="100px">
|
||||
</a>
|
||||
|
||||
### 🥈 Silver Sponsors
|
||||
@@ -149,11 +136,9 @@ Your financial contributions help ensure Blitz continues to be developed and mai
|
||||
|
||||
### 🏆 Gold Sponsors
|
||||
|
||||
<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 aria-label="Your Company" href="#">
|
||||
<img alt="" src="https://dummyimage.com/1000x330/efe8ff/000000.png&text=Your+Logo+Here" width="300px">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 💎 Diamond Sponsors
|
||||
|
||||
@@ -169,6 +154,8 @@ Your financial contributions help ensure Blitz continues to be developed and mai
|
||||
<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 />Creator</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 />Node.js Wizard</td>
|
||||
<td align="center"><a href="https://merelinguist.now.sh"><img src="https://avatars3.githubusercontent.com/u/24858006?v=4" width="100px;" alt=""/><br /><sub><b>Dylan Brookes</b></sub></a><br />Friendly Generalist</td>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-enable -->
|
||||
@@ -187,7 +174,6 @@ _Code ownership, pull request approvals and merging, etc_ (see [MAINTAINERS.md](
|
||||
<tr>
|
||||
<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>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-enable -->
|
||||
@@ -206,18 +192,15 @@ _Issue triage, pull request triage, community encouragement and moderation, etc_
|
||||
<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://simonknott.de"><img src="https://avatars1.githubusercontent.com/u/14912729?v=4" width="100px;" alt=""/><br /><sub><b>Simon Knott</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://simonpeterdebbarma.com"><img src="https://avatars3.githubusercontent.com/u/31207418?v=4" width="100px;" alt=""/><br /><sub><b>Simon Debbarma</b></sub></a></td>
|
||||
<td align="center"><a href="https://github.com/jclancy93"><img src="https://avatars2.githubusercontent.com/u/7850202?v=4" width="100px;" alt=""/><br /><sub><b>Jack Clancy</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>
|
||||
<td align="center"><a href="https://twitter.com/ivandevp"><img src="https://avatars3.githubusercontent.com/u/9284690?v=4" width="100px;" alt=""/><br /><sub><b>Ivan Medina</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>
|
||||
</tr>
|
||||
</table>
|
||||
<!-- markdownlint-enable -->
|
||||
@@ -312,7 +295,7 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="https://kevo.dev"><img src="https://avatars3.githubusercontent.com/u/15717067?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Tovar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=kevotovar" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://anteprimorac.com.hr"><img src="https://avatars0.githubusercontent.com/u/972083?v=4" width="100px;" alt=""/><br /><sub><b>Ante Primorac</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=anteprimorac" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://mykalmachon.dev"><img src="https://avatars1.githubusercontent.com/u/7844994?v=4" width="100px;" alt=""/><br /><sub><b>Mykal Machon</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=MykalMachon" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://jamiedavenport.dev"><img src="https://avatars2.githubusercontent.com/u/1329874?v=4" width="100px;" alt=""/><br /><sub><b>Jamie Davenport</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jamiedavenport" title="Code">💻</a> <a href="#maintenance-jamiedavenport" title="Maintenance">🚧</a></td>
|
||||
<td align="center"><a href="https://jamiedavenport.dev"><img src="https://avatars2.githubusercontent.com/u/1329874?v=4" width="100px;" alt=""/><br /><sub><b>Jamie Davenport</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jamiedavenport" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://cloudnweb.dev/"><img src="https://avatars0.githubusercontent.com/u/17050715?v=4" width="100px;" alt=""/><br /><sub><b>GaneshMani</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ganeshmani" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -365,9 +348,9 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="https://twitter.com/dillonraphael"><img src="https://avatars0.githubusercontent.com/u/3496193?v=4" width="100px;" alt=""/><br /><sub><b>Dillon Raphael</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dillonraphael" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/clgeoio"><img src="https://avatars2.githubusercontent.com/u/37571416?v=4" width="100px;" alt=""/><br /><sub><b>Cody G</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=clgeoio" title="Tests">⚠️</a></td>
|
||||
<td align="center"><a href="https://github.com/madflow"><img src="https://avatars0.githubusercontent.com/u/183248?v=4" width="100px;" alt=""/><br /><sub><b>madflow</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=madflow" title="Documentation">📖</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><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a> <a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a> <a href="#maintenance-engelkes-finstreet" title="Maintenance">🚧</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><br /><a href="https://github.com/blitz-js/blitz/commits?author=nitaking" title="Code">💻</a> <a href="#maintenance-nitaking" title="Maintenance">🚧</a> <a href="#question-nitaking" title="Answering Questions">💬</a></td>
|
||||
<td align="center"><a href="https://github.com/sirmyron"><img src="https://avatars2.githubusercontent.com/u/1430136?v=4" width="100px;" alt=""/><br /><sub><b>sirmyron</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sirmyron" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/engelkes-finstreet"><img src="https://avatars1.githubusercontent.com/u/36962022?v=4" width="100px;" alt=""/><br /><sub><b>engelkes-finstreet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=engelkes-finstreet" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://twitter.com/pixelscommander"><img src="https://avatars2.githubusercontent.com/u/810671?v=4" width="100px;" alt=""/><br /><sub><b>Denis Radin</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/pulls?q=is%3Apr+reviewed-by%3APixelsCommander" title="Reviewed Pull Requests">👀</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=PixelsCommander" title="Documentation">📖</a></td>
|
||||
@@ -403,88 +386,6 @@ Thanks to these wonderful people ([emoji key](https://allcontributors.org/docs/e
|
||||
<td align="center"><a href="https://github.com/konradkalemba"><img src="https://avatars0.githubusercontent.com/u/8682104?v=4" width="100px;" alt=""/><br /><sub><b>Konrad Kalemba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=konradkalemba" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Alucard17"><img src="https://avatars1.githubusercontent.com/u/26205172?v=4" width="100px;" alt=""/><br /><sub><b>Alucard17</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Alucard17" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/Dohxis"><img src="https://avatars2.githubusercontent.com/u/8768909?v=4" width="100px;" alt=""/><br /><sub><b>Domantas Mauruča</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=Dohxis" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://sandulat.com/"><img src="https://avatars0.githubusercontent.com/u/7345874?v=4" width="100px;" alt=""/><br /><sub><b>Stratulat Alexandru</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sandulat" title="Code">💻</a> <a href="#maintenance-sandulat" title="Maintenance">🚧</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/aericson"><img src="https://avatars3.githubusercontent.com/u/692542?v=4" width="100px;" alt=""/><br /><sub><b>André Ericson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=aericson" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=aericson" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="http://Cajotafer.com"><img src="https://avatars2.githubusercontent.com/u/41461969?v=4" width="100px;" alt=""/><br /><sub><b>Carlos Fernández</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cajotafer" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://oesterkilde.dk/"><img src="https://avatars1.githubusercontent.com/u/6379824?v=4" width="100px;" alt=""/><br /><sub><b>Kevin Østerkilde</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Kosai106" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=Kosai106" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/aaronfulkerson"><img src="https://avatars0.githubusercontent.com/u/31112737?v=4" width="100px;" alt=""/><br /><sub><b>aaronfulkerson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=aaronfulkerson" title="Code">💻</a> <a href="#question-aaronfulkerson" title="Answering Questions">💬</a></td>
|
||||
<td align="center"><a href="https://github.com/alexnaiman"><img src="https://avatars3.githubusercontent.com/u/25799714?v=4" width="100px;" alt=""/><br /><sub><b>Alexandru Naiman</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=alexnaiman" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://davidlutta.github.io/portfolio/"><img src="https://avatars2.githubusercontent.com/u/14890315?v=4" width="100px;" alt=""/><br /><sub><b>David Ezekiel Lutta</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=davidlutta" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/wanjuntham"><img src="https://avatars1.githubusercontent.com/u/49380551?v=4" width="100px;" alt=""/><br /><sub><b>wanjuntham</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=wanjuntham" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://www.nahuelchaves.xyz"><img src="https://avatars3.githubusercontent.com/u/96837?v=4" width="100px;" alt=""/><br /><sub><b>Victor Nahuel Chaves</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nahue" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/peter50216"><img src="https://avatars3.githubusercontent.com/u/891109?v=4" width="100px;" alt=""/><br /><sub><b>Peter Shih</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=peter50216" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://seweryn.kale.mba"><img src="https://avatars3.githubusercontent.com/u/37031328?v=4" width="100px;" alt=""/><br /><sub><b>Seweryn Kalemba</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sewerynkalemba" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://nksaraf.github.io"><img src="https://avatars2.githubusercontent.com/u/11255148?v=4" width="100px;" alt=""/><br /><sub><b>Nikhil Saraf</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nksaraf" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=nksaraf" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://zane.sh"><img src="https://avatars0.githubusercontent.com/u/16865690?v=4" width="100px;" alt=""/><br /><sub><b>Zane</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=zanedb" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/dulcehc"><img src="https://avatars1.githubusercontent.com/u/19391835?v=4" width="100px;" alt=""/><br /><sub><b>Dulce Hernández</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=dulcehc" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://markhaehnel.de"><img src="https://avatars2.githubusercontent.com/u/1516205?v=4" width="100px;" alt=""/><br /><sub><b>Mark Hähnel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=markhaehnel" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="http://stackoverflow.com/users/872395/nemesv"><img src="https://avatars0.githubusercontent.com/u/251330?v=4" width="100px;" alt=""/><br /><sub><b>Viktor Nemes</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=nemesv" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://gabeoleary.com"><img src="https://avatars1.githubusercontent.com/u/16123225?v=4" width="100px;" alt=""/><br /><sub><b>Gabe O'Leary</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=goleary" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/machadolucasvp"><img src="https://avatars0.githubusercontent.com/u/44952113?v=4" width="100px;" alt=""/><br /><sub><b>Lucas Machado</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=machadolucasvp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/maciekgrzybek"><img src="https://avatars2.githubusercontent.com/u/16546428?v=4" width="100px;" alt=""/><br /><sub><b>maciek_grzybek</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=maciekgrzybek" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/mweibel"><img src="https://avatars1.githubusercontent.com/u/307427?v=4" width="100px;" alt=""/><br /><sub><b>Michael Weibel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mweibel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://isoppp.com"><img src="https://avatars0.githubusercontent.com/u/16318727?v=4" width="100px;" alt=""/><br /><sub><b>Hiroki Isogai</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=isoppp" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/matamatanot"><img src="https://avatars2.githubusercontent.com/u/39780486?v=4" width="100px;" alt=""/><br /><sub><b>matamatanot</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=matamatanot" title="Documentation">📖</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/ericsakmar"><img src="https://avatars3.githubusercontent.com/u/5620709?v=4" width="100px;" alt=""/><br /><sub><b>Eric Sakmar</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ericsakmar" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://github.com/leggsimon"><img src="https://avatars2.githubusercontent.com/u/11544418?v=4" width="100px;" alt=""/><br /><sub><b>Simon Legg</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=leggsimon" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://robsoriano.com"><img src="https://avatars3.githubusercontent.com/u/13049130?v=4" width="100px;" alt=""/><br /><sub><b>Robert Soriano</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=wobsoriano" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/benediktms"><img src="https://avatars2.githubusercontent.com/u/48836135?v=4" width="100px;" alt=""/><br /><sub><b>Benedikt Schnatterbeck</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=benediktms" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://taloranderson.com"><img src="https://avatars2.githubusercontent.com/u/11509865?v=4" width="100px;" alt=""/><br /><sub><b>Talor Anderson</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Talor-A" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/akirabaruah"><img src="https://avatars2.githubusercontent.com/u/6751517?v=4" width="100px;" alt=""/><br /><sub><b>Akira Baruah</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=akirabaruah" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://chriswray.dev/"><img src="https://avatars0.githubusercontent.com/u/53663762?v=4" width="100px;" alt=""/><br /><sub><b>Christopher Wray</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=cwray-tech" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/piotrski"><img src="https://avatars0.githubusercontent.com/u/244174?v=4" width="100px;" alt=""/><br /><sub><b>Piotrek Tomczewski</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=piotrski" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://raph.site"><img src="https://avatars3.githubusercontent.com/u/1575946?v=4" width="100px;" alt=""/><br /><sub><b>Raphaël Huchet</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=rap2hpoutre" title="Documentation">📖</a> <a href="https://github.com/blitz-js/blitz/commits?author=rap2hpoutre" title="Tests">⚠️</a> <a href="https://github.com/blitz-js/blitz/commits?author=rap2hpoutre" title="Code">💻</a></td>
|
||||
<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></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>
|
||||
<td align="center"><a href="https://github.com/sakulstra"><img src="https://avatars3.githubusercontent.com/u/4396533?v=4" width="100px;" alt=""/><br /><sub><b>Lukas Strassel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=sakulstra" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://thibpat.com"><img src="https://avatars3.githubusercontent.com/u/494686?v=4" width="100px;" alt=""/><br /><sub><b>Thibaut Patel</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tpatel" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://jonstuebe.com"><img src="https://avatars0.githubusercontent.com/u/156722?v=4" width="100px;" alt=""/><br /><sub><b>Jon Stuebe</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jonstuebe" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://ugogo.dev"><img src="https://avatars2.githubusercontent.com/u/5040476?v=4" width="100px;" alt=""/><br /><sub><b>Ugo Onali</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=ugogo" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://saintmalik.me"><img src="https://avatars1.githubusercontent.com/u/37118134?v=4" width="100px;" alt=""/><br /><sub><b>SaintMalik</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=saintmalik" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://khaledgarbaya.net"><img src="https://avatars1.githubusercontent.com/u/1156093?v=4" width="100px;" alt=""/><br /><sub><b>Khaled Garbaya</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=Khaledgarbaya" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://tundera.dev"><img src="https://avatars0.githubusercontent.com/u/61833561?v=4" width="100px;" alt=""/><br /><sub><b>tundera</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=tundera" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="https://github.com/markylaing"><img src="https://avatars2.githubusercontent.com/u/41469221?v=4" width="100px;" alt=""/><br /><sub><b>markylaing</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=markylaing" title="Code">💻</a> <a href="https://github.com/blitz-js/blitz/commits?author=markylaing" title="Documentation">📖</a></td>
|
||||
<td align="center"><a href="https://akfm.dev/"><img src="https://avatars2.githubusercontent.com/u/25711332?v=4" width="100px;" alt=""/><br /><sub><b>Akifumi Sato</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=AkifumiSato" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/beeplin"><img src="https://avatars3.githubusercontent.com/u/13058150?v=4" width="100px;" alt=""/><br /><sub><b>Beep LIN</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=beeplin" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://mattwood.tech/"><img src="https://avatars1.githubusercontent.com/u/22530815?v=4" width="100px;" alt=""/><br /><sub><b>Matt Wood</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=mattfwood" title="Code">💻</a></td>
|
||||
<td align="center"><a href="http://joaquin.axai.mx"><img src="https://avatars1.githubusercontent.com/u/15214?v=4" width="100px;" alt=""/><br /><sub><b>Joaquin Bravo Contreras</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=jackbravo" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/arjundubey-cr"><img src="https://avatars0.githubusercontent.com/u/40758425?v=4" width="100px;" alt=""/><br /><sub><b>Arjun Dubey</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=arjundubey-cr" title="Code">💻</a></td>
|
||||
<td align="center"><a href="https://github.com/chanand"><img src="https://avatars0.githubusercontent.com/u/1317789?v=4" width="100px;" alt=""/><br /><sub><b>chanand</b></sub></a><br /><a href="https://github.com/blitz-js/blitz/commits?author=chanand" title="Code">💻</a></td>
|
||||
</tr>
|
||||
<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></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 5.9 KiB |
@@ -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,7 +14,7 @@ model Project {
|
||||
2. DB migrate
|
||||
|
||||
```
|
||||
blitz prisma migrate dev --preview-feature
|
||||
blitz db migrate
|
||||
```
|
||||
|
||||
3. Start the dev server
|
||||
|
||||
@@ -23,115 +23,109 @@ assert(process.env.AUTH0_CLIENT_SECRET, "You must provide the AUTH0_CLIENT_SECRE
|
||||
|
||||
export default passportAuth({
|
||||
successRedirectUrl: "/",
|
||||
authenticateOptions: {scope: "openid email profile"}, //used for Auth0Strategy - without an empty profile is returned
|
||||
strategies: [
|
||||
{
|
||||
strategy: new TwitterStrategy(
|
||||
{
|
||||
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"
|
||||
: "http://localhost:3000/api/auth/twitter/callback",
|
||||
includeEmail: true,
|
||||
},
|
||||
async function (_token, _tokenSecret, profile, done) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
new TwitterStrategy(
|
||||
{
|
||||
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"
|
||||
: "http://localhost:3000/api/auth/twitter/callback",
|
||||
includeEmail: true,
|
||||
},
|
||||
async function (_token, _tokenSecret, profile, done) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("Twitter OAuth response doesn't have email."))
|
||||
}
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("Twitter OAuth response doesn't have email."))
|
||||
}
|
||||
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
|
||||
const publicData = {userId: user.id, roles: [user.role], source: "twitter"}
|
||||
done(null, {publicData})
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
strategy: new GitHubStrategy(
|
||||
{
|
||||
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, _tokenSecret, profile, done) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
const publicData = {userId: user.id, roles: [user.role], source: "twitter"}
|
||||
done(null, {publicData})
|
||||
},
|
||||
),
|
||||
new GitHubStrategy(
|
||||
{
|
||||
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, _tokenSecret, profile, done) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("Twitter OAuth response doesn't have email."))
|
||||
}
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("Twitter OAuth response doesn't have email."))
|
||||
}
|
||||
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
|
||||
const publicData = {
|
||||
userId: user.id,
|
||||
roles: [user.role],
|
||||
source: "github",
|
||||
githubUsername: profile.username,
|
||||
}
|
||||
done(null, {publicData})
|
||||
},
|
||||
),
|
||||
},
|
||||
{
|
||||
authenticateOptions: {scope: "openid email profile"},
|
||||
strategy: new Auth0Strategy(
|
||||
{
|
||||
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) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
const publicData = {
|
||||
userId: user.id,
|
||||
roles: [user.role],
|
||||
source: "github",
|
||||
githubUsername: profile.username,
|
||||
}
|
||||
done(null, {publicData})
|
||||
},
|
||||
),
|
||||
new Auth0Strategy(
|
||||
{
|
||||
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) {
|
||||
const email = profile.emails && profile.emails[0]?.value
|
||||
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("Auth0 response doesn't have email."))
|
||||
}
|
||||
if (!email) {
|
||||
// This can happen if you haven't enabled email access in your twitter app permissions
|
||||
return done(new Error("GitHub OAuth response doesn't have email."))
|
||||
}
|
||||
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
const user = await db.user.upsert({
|
||||
where: {email},
|
||||
create: {
|
||||
email,
|
||||
name: profile.displayName,
|
||||
},
|
||||
update: {email},
|
||||
})
|
||||
|
||||
const publicData = {
|
||||
userId: user.id,
|
||||
roles: [user.role],
|
||||
source: "auth0",
|
||||
githubUsername: profile.username,
|
||||
}
|
||||
done(undefined, {publicData})
|
||||
},
|
||||
),
|
||||
},
|
||||
const publicData = {
|
||||
userId: user.id,
|
||||
roles: [user.role],
|
||||
source: "auth0",
|
||||
githubUsername: profile.username,
|
||||
}
|
||||
done(undefined, {publicData})
|
||||
},
|
||||
),
|
||||
],
|
||||
})
|
||||
|
||||
39
examples/auth/app/auth/auth-utils.ts
Normal file
39
examples/auth/app/auth/auth-utils.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import {AuthenticationError} from "blitz"
|
||||
import SecurePassword from "secure-password"
|
||||
import db from "db"
|
||||
|
||||
const SP = new SecurePassword()
|
||||
|
||||
export const hashPassword = async (password: string) => {
|
||||
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"))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const authenticateUser = async (email: string, password: string) => {
|
||||
const user = await db.user.findOne({where: {email}})
|
||||
|
||||
if (!user || !user.hashedPassword) throw new AuthenticationError()
|
||||
|
||||
switch (await verifyPassword(user.hashedPassword, password)) {
|
||||
case SecurePassword.VALID:
|
||||
break
|
||||
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}})
|
||||
break
|
||||
default:
|
||||
throw new AuthenticationError()
|
||||
}
|
||||
|
||||
const {hashedPassword, ...rest} = user
|
||||
return rest
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import {Link, useMutation, AuthenticationError} from "blitz"
|
||||
import {Link, useMutation} from "blitz"
|
||||
import {LabeledTextField} from "app/components/LabeledTextField"
|
||||
import {Form, FORM_ERROR} from "app/components/Form"
|
||||
import login, {LoginInput} from "app/auth/mutations/login"
|
||||
import login from "app/auth/mutations/login"
|
||||
import {LoginInput} from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
@@ -13,7 +14,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
<Form
|
||||
submitText="Login"
|
||||
submitText="Log In"
|
||||
schema={LoginInput}
|
||||
initialValues={{email: undefined, password: undefined}}
|
||||
onSubmit={async (values) => {
|
||||
@@ -21,7 +22,7 @@ export const LoginForm = (props: LoginFormProps) => {
|
||||
await loginMutation(values)
|
||||
props.onSuccess && props.onSuccess()
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
return {[FORM_ERROR]: "Sorry, those credentials are invalid"}
|
||||
} else {
|
||||
return {
|
||||
|
||||
@@ -1,28 +1,6 @@
|
||||
import {Ctx, SecurePassword, AuthenticationError} from "blitz"
|
||||
import db from "db"
|
||||
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()
|
||||
|
||||
const result = await SecurePassword.verify(user.hashedPassword, password)
|
||||
|
||||
if (result === SecurePassword.VALID_NEEDS_REHASH) {
|
||||
// Upgrade hashed password with a more secure hash
|
||||
const improvedHash = await SecurePassword.hash(password)
|
||||
await db.user.update({where: {id: user.id}, data: {hashedPassword: improvedHash}})
|
||||
}
|
||||
|
||||
const {hashedPassword, ...rest} = user
|
||||
return rest
|
||||
}
|
||||
|
||||
export const LoginInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
})
|
||||
export type LoginInputType = z.infer<typeof LoginInput>
|
||||
import {Ctx} from "blitz"
|
||||
import {authenticateUser} from "app/auth/auth-utils"
|
||||
import {LoginInput, LoginInputType} from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, {session}: Ctx) {
|
||||
// This throws an error if input is invalid
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import {Ctx, SecurePassword} from "blitz"
|
||||
import {Ctx} from "blitz"
|
||||
import db from "db"
|
||||
import * as z from "zod"
|
||||
|
||||
export const SignupInput = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(10).max(100),
|
||||
})
|
||||
export type SignupInputType = z.infer<typeof SignupInput>
|
||||
import {hashPassword} from "app/auth/auth-utils"
|
||||
import {SignupInput, SignupInputType} from "app/auth/validations"
|
||||
|
||||
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 SecurePassword.hash(password)
|
||||
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},
|
||||
|
||||
@@ -7,7 +7,7 @@ const SignupPage: BlitzPage = () => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Login</title>
|
||||
<title>Log In</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
import {SignupInput} from "app/auth/validations"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {useQuery} from "blitz"
|
||||
import {useQuery, useSession} from "blitz"
|
||||
import getCurrentUser from "app/users/queries/getCurrentUser"
|
||||
|
||||
export const useCurrentUser = () => {
|
||||
const [user] = useQuery(getCurrentUser, null)
|
||||
return user
|
||||
// We wouldn't have to useSession() here, but doing so improves perf on initial
|
||||
// load since we can skip the getCurrentUser() request.
|
||||
const session = useSession()
|
||||
const [user] = useQuery(getCurrentUser, null, {enabled: !!session.userId})
|
||||
return session.userId ? user : null
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {AppProps, ErrorComponent, useRouter, AuthenticationError, AuthorizationError} from "blitz"
|
||||
import {AppProps, ErrorComponent, useRouter} from "blitz"
|
||||
import {ErrorBoundary} from "react-error-boundary"
|
||||
import {queryCache} from "react-query"
|
||||
import LoginForm from "app/auth/components/LoginForm"
|
||||
@@ -25,9 +25,9 @@ export default function App({Component, pageProps}: AppProps) {
|
||||
}
|
||||
|
||||
function RootErrorFallback({error, resetErrorBoundary}) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
return <LoginForm onSuccess={resetErrorBoundary} />
|
||||
} else if (error instanceof AuthorizationError) {
|
||||
} else if (error.name === "AuthorizationError") {
|
||||
return (
|
||||
<ErrorComponent
|
||||
statusCode={error.statusCode}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import {render} from "test/utils"
|
||||
|
||||
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", () => {
|
||||
mockUseCurrentUser.mockReturnValue({
|
||||
id: 1,
|
||||
name: "User",
|
||||
email: "user@email.com",
|
||||
role: "user",
|
||||
})
|
||||
|
||||
const {getByText} = render(<Home />)
|
||||
const element = getByText(/powered by blitz/i)
|
||||
expect(element).toBeInTheDocument()
|
||||
})
|
||||
@@ -25,6 +25,8 @@ const UserStuff = () => {
|
||||
|
||||
if (session.isLoading) return <div>Loading...</div>
|
||||
|
||||
console.log(session.views)
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!session.userId && (
|
||||
@@ -33,7 +35,7 @@ const UserStuff = () => {
|
||||
<Link href="/signup">Sign Up</Link>
|
||||
</div>
|
||||
<div>
|
||||
<Link href="/login">Login</Link>
|
||||
<Link href="/login">Log In</Link>
|
||||
</div>
|
||||
<a href="/api/auth/twitter" style={{display: "block"}}>
|
||||
Login with Twitter
|
||||
|
||||
@@ -7,8 +7,6 @@ import {
|
||||
PromiseReturnType,
|
||||
ErrorComponent as ErrorPage,
|
||||
useMutation,
|
||||
AuthenticationError,
|
||||
AuthorizationError,
|
||||
} from "blitz"
|
||||
import getUser from "app/users/queries/getUser"
|
||||
import logout from "app/auth/mutations/logout"
|
||||
@@ -44,10 +42,10 @@ export const getServerSideProps: GetServerSideProps<PageProps> = async ({req, re
|
||||
res.statusCode = 404
|
||||
res.end()
|
||||
return {props: {}}
|
||||
} else if (error instanceof AuthenticationError) {
|
||||
} else if (error.name === "AuthenticationError") {
|
||||
res.writeHead(302, {location: "/login"}).end()
|
||||
return {props: {}}
|
||||
} else if (error instanceof AuthorizationError) {
|
||||
} else if (error.name === "AuthorizationError") {
|
||||
return {
|
||||
props: {
|
||||
error: {
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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
|
||||
@@ -4,7 +4,7 @@ import db from "db"
|
||||
export default async function getCurrentUser(_ = null, ctx: Ctx) {
|
||||
if (!ctx.session.userId) return null
|
||||
|
||||
const user = await db.user.findFirst({
|
||||
const user = await db.user.findOne({
|
||||
where: {id: ctx.session.userId},
|
||||
select: {id: true, name: true, email: true, role: true},
|
||||
})
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import {Ctx, NotFoundError} from "blitz"
|
||||
import db, {Prisma} from "db"
|
||||
import db, {FindOneUserArgs} from "db"
|
||||
|
||||
type GetUserInput = {
|
||||
where: Prisma.FindUniqueUserArgs["where"]
|
||||
where: FindOneUserArgs["where"]
|
||||
}
|
||||
|
||||
export default async function getUser({where}: GetUserInput, ctx: Ctx) {
|
||||
ctx.session.authorize()
|
||||
console.log(ctx.session.userId)
|
||||
|
||||
const user = await db.user.findFirst({where})
|
||||
const user = await db.user.findOne({where})
|
||||
|
||||
if (!user) throw new NotFoundError(`User with id ${where.id} does not exist`)
|
||||
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
import {Ctx} from "blitz"
|
||||
import db, {FindManyUserArgs} from "db"
|
||||
|
||||
type GetUsersInput = Pick<FindManyUserArgs, "where" | "orderBy" | "skip" | "take">
|
||||
type GetUsersInput = {
|
||||
where?: FindManyUserArgs["where"]
|
||||
orderBy?: FindManyUserArgs["orderBy"]
|
||||
cursor?: FindManyUserArgs["cursor"]
|
||||
take?: FindManyUserArgs["take"]
|
||||
skip?: FindManyUserArgs["skip"]
|
||||
}
|
||||
|
||||
export default async function getUsers({where, orderBy, skip = 0, take}: GetUsersInput, ctx: Ctx) {
|
||||
ctx.session.authorize()
|
||||
export default async function getUsers(
|
||||
{where, orderBy, cursor, take, skip}: GetUsersInput,
|
||||
ctx: Ctx,
|
||||
) {
|
||||
ctx.session.authorize(["admin", "user"])
|
||||
|
||||
const users = await db.user.findMany({
|
||||
where,
|
||||
select: {id: true},
|
||||
orderBy,
|
||||
cursor,
|
||||
take,
|
||||
skip,
|
||||
})
|
||||
|
||||
const count = await db.user.count()
|
||||
const hasMore = typeof take === "number" ? skip + take < count : false
|
||||
const nextPage = hasMore ? {take, skip: skip + take!} : null
|
||||
|
||||
return {
|
||||
users,
|
||||
nextPage,
|
||||
hasMore,
|
||||
count,
|
||||
}
|
||||
return users
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const {sessionMiddleware, simpleRolesIsAuthorized} = require("@blitzjs/server")
|
||||
const {sessionMiddleware, unstable_simpleRolesIsAuthorized} = require("@blitzjs/server")
|
||||
const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
enabled: process.env.ANALYZE === "true",
|
||||
})
|
||||
@@ -6,16 +6,10 @@ const withBundleAnalyzer = require("@next/bundle-analyzer")({
|
||||
module.exports = withBundleAnalyzer({
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
isAuthorized: simpleRolesIsAuthorized,
|
||||
unstable_isAuthorized: unstable_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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3099",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"video": false,
|
||||
"chromeWebSecurity": false
|
||||
"video": false
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ describe("index page", () => {
|
||||
})
|
||||
|
||||
it("goes to the login page", () => {
|
||||
cy.contains("a", /login/i).click()
|
||||
cy.contains("a", "Log In").click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
})
|
||||
|
||||
@@ -30,11 +30,11 @@ describe("index page", () => {
|
||||
cy.signup(user)
|
||||
|
||||
cy.contains("button", "Logout").click()
|
||||
cy.contains("a", /login/i).click()
|
||||
cy.contains("a", "Log In").click()
|
||||
|
||||
cy.contains("Email").find("input").type(user.email)
|
||||
cy.contains("Password").find("input").type(user.password)
|
||||
cy.contains("button", /login/i).click()
|
||||
cy.contains("button", "Log In").click()
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.contains("button", "Logout")
|
||||
@@ -48,7 +48,7 @@ describe("index page", () => {
|
||||
cy.contains("button", "Logout").click()
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.contains("a", /login/i)
|
||||
cy.contains("a", "Log In")
|
||||
})
|
||||
|
||||
it("tracks anonymous sessions", () => {
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import {makeServerOnlyPrisma} from "blitz"
|
||||
import {PrismaClient} from "@prisma/client"
|
||||
|
||||
const ServerOnlyPrisma = makeServerOnlyPrisma(PrismaClient)
|
||||
|
||||
export * from "@prisma/client"
|
||||
export default new ServerOnlyPrisma()
|
||||
|
||||
let prisma: PrismaClient
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
prisma = new PrismaClient()
|
||||
} else {
|
||||
// Ensure the prisma instance is re-used during hot-reloading
|
||||
// Otherwise, a new client will be created on every reload
|
||||
globalThis["prisma"] = globalThis["prisma"] || new PrismaClient()
|
||||
prisma = globalThis["prisma"]
|
||||
}
|
||||
|
||||
export default 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");
|
||||
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,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 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "blitz",
|
||||
}
|
||||
@@ -1,18 +1,16 @@
|
||||
{
|
||||
"name": "@examples/auth",
|
||||
"version": "0.29.2",
|
||||
"version": "0.24.0-canary.3",
|
||||
"scripts": {
|
||||
"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",
|
||||
"test": "prisma generate && yarn test:jest && yarn test:e2e",
|
||||
"test:jest": "jest",
|
||||
"test:server": "blitz prisma migrate deploy --preview-feature && 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"
|
||||
"test:start": "blitz db migrate && blitz start --production -p 3099",
|
||||
"test": "cross-env NODE_ENV=test start-server-and-test test:start http://localhost:3099 cy:run"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
@@ -38,37 +36,47 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.14.0",
|
||||
"@prisma/client": "2.14.0",
|
||||
"blitz": "0.29.2",
|
||||
"@prisma/cli": "2.8.0",
|
||||
"@prisma/client": "2.8.0",
|
||||
"blitz": "0.24.0-canary.3",
|
||||
"final-form": "4.20.1",
|
||||
"passport-auth0": "1.4.0",
|
||||
"passport-github2": "0.1.12",
|
||||
"passport-auth0": "1.3.3",
|
||||
"passport-github2": "0.1.11",
|
||||
"passport-twitter": "1.0.4",
|
||||
"react": "0.0.0-experimental-3310209d0",
|
||||
"react-dom": "0.0.0-experimental-3310209d0",
|
||||
"react-error-boundary": "3.1.0",
|
||||
"react-final-form": "6.5.2",
|
||||
"zod": "1.11.11"
|
||||
"react": "0.0.0-experimental-7f28234f8",
|
||||
"react-dom": "0.0.0-experimental-7f28234f8",
|
||||
"react-error-boundary": "2.3.1",
|
||||
"react-final-form": "6.5.1",
|
||||
"secure-password": "4.0.0",
|
||||
"zod": "1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "2.5.1",
|
||||
"@cypress/skip-test": "2.5.0",
|
||||
"@next/bundle-analyzer": "latest",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/passport-auth0": "1.0.4",
|
||||
"@types/passport-github2": "1.2.4",
|
||||
"@types/passport-twitter": "1.0.36",
|
||||
"@types/react": "17.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "7.17.0",
|
||||
"husky": "4.3.7",
|
||||
"lint-staged": "10.5.3",
|
||||
"prettier": "2.2.1",
|
||||
"pretty-quick": "3.1.0",
|
||||
"start-server-and-test": "1.11.7",
|
||||
"typescript": "4.1.3"
|
||||
"@types/react": "16.9.38",
|
||||
"@types/secure-password": "3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "2.34.1-alpha.2",
|
||||
"@typescript-eslint/parser": "2.34.1-alpha.2",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cross-env": "latest",
|
||||
"cypress": "4.11.0",
|
||||
"eslint": "7.6.0",
|
||||
"eslint-config-react-app": "5.2.1",
|
||||
"eslint-plugin-cypress": "2.11.1",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-jsx-a11y": "6.3.1",
|
||||
"eslint-plugin-react": "7.20.5",
|
||||
"eslint-plugin-react-hooks": "4.0.8",
|
||||
"husky": "4.2.5",
|
||||
"lint-staged": "10.2.13",
|
||||
"prettier": "2.0.5",
|
||||
"pretty-quick": "2.0.1",
|
||||
"start-server-and-test": "1.11.2",
|
||||
"typescript": "3.9.5"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// This is the jest 'setupFilesAfterEnv' setup file
|
||||
// It's a good place to set globals, add global before/after hooks, etc
|
||||
@@ -1,89 +0,0 @@
|
||||
import {RouterContext, BlitzRouter} from "blitz"
|
||||
import {render as defaultRender} from "@testing-library/react"
|
||||
import {renderHook as defaultRenderHook} from "@testing-library/react-hooks"
|
||||
|
||||
export * from "@testing-library/react"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// This file customizes the render() and renderHook() test functions provided
|
||||
// by React testing library. It adds a router context wrapper with a mocked router.
|
||||
//
|
||||
// You should always import `render` and `renderHook` from this file
|
||||
//
|
||||
// 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()
|
||||
// --------------------------------------------------
|
||||
// Override the default test render with our own
|
||||
//
|
||||
// You can override the router mock like this:
|
||||
//
|
||||
// const { baseElement } = render(<MyComponent />, {
|
||||
// router: { pathname: '/my-custom-pathname' },
|
||||
// });
|
||||
// --------------------------------------------------
|
||||
export function render(ui: RenderUI, {wrapper, router, ...options}: RenderOptions = {}) {
|
||||
if (!wrapper) {
|
||||
// Add a default context wrapper if one isn't supplied from the test
|
||||
wrapper = ({children}) => (
|
||||
<RouterContext.Provider value={{...mockRouter, ...router}}>{children}</RouterContext.Provider>
|
||||
)
|
||||
}
|
||||
return defaultRender(ui, {wrapper, ...options})
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// renderHook()
|
||||
// --------------------------------------------------
|
||||
// Override the default test renderHook with our own
|
||||
//
|
||||
// You can override the router mock like this:
|
||||
//
|
||||
// const result = renderHook(() => myHook(), {
|
||||
// router: { pathname: '/my-custom-pathname' },
|
||||
// });
|
||||
// --------------------------------------------------
|
||||
export function renderHook(
|
||||
hook: RenderHook,
|
||||
{wrapper, router, ...options}: RenderHookOptions = {},
|
||||
) {
|
||||
if (!wrapper) {
|
||||
// Add a default context wrapper if one isn't supplied from the test
|
||||
wrapper = ({children}) => (
|
||||
<RouterContext.Provider value={{...mockRouter, ...router}}>{children}</RouterContext.Provider>
|
||||
)
|
||||
}
|
||||
return defaultRenderHook(hook, {wrapper, ...options})
|
||||
}
|
||||
|
||||
export const mockRouter: BlitzRouter = {
|
||||
basePath: "",
|
||||
pathname: "/",
|
||||
route: "/",
|
||||
asPath: "/",
|
||||
params: {},
|
||||
query: {},
|
||||
isReady: true,
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
back: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
beforePopState: jest.fn(),
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
isFallback: false,
|
||||
}
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
SESSION_SECRET_KEY=A4B979A108440A218DE8F11CBB13A7B0
|
||||
@@ -1,14 +0,0 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser",
|
||||
env: {
|
||||
es2020: true,
|
||||
},
|
||||
extends: ["react-app", "plugin:jsx-a11y/recommended"],
|
||||
plugins: ["@typescript-eslint", "jsx-a11y"],
|
||||
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
|
||||
},
|
||||
}
|
||||
55
examples/custom-server/.gitignore
vendored
55
examples/custom-server/.gitignore
vendored
@@ -1,55 +0,0 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.pnp.*
|
||||
.npm
|
||||
web_modules/
|
||||
|
||||
# blitz
|
||||
/.blitz/
|
||||
/.next/
|
||||
*.sqlite
|
||||
.now
|
||||
.blitz-console-history
|
||||
blitz-log.log
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
||||
# local env files
|
||||
.env.local
|
||||
.env.*.local
|
||||
.envrc
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Testing
|
||||
.coverage
|
||||
*.lcov
|
||||
.nyc_output
|
||||
lib-cov
|
||||
|
||||
# Caches
|
||||
*.tsbuildinfo
|
||||
.eslintcache
|
||||
.node_repl_history
|
||||
.yarn-integrity
|
||||
|
||||
# Serverless directories
|
||||
.serverless/
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
.vscode-test
|
||||
|
||||
# Custom build
|
||||
build
|
||||
@@ -1,7 +0,0 @@
|
||||
.gitkeep
|
||||
.env*
|
||||
*.ico
|
||||
*.lock
|
||||
db/migrations
|
||||
.next
|
||||
.blitz
|
||||
@@ -1,29 +0,0 @@
|
||||
# custom-server
|
||||
|
||||
## Development
|
||||
|
||||
1. Install
|
||||
```sh
|
||||
yarn
|
||||
```
|
||||
2. Migrate
|
||||
```sh
|
||||
blitz prisma migrate dev --preview-feature
|
||||
```
|
||||
3. Start the dev server
|
||||
|
||||
```sh
|
||||
blitz start
|
||||
|
||||
// Or if you want hot-reloading of server.js, use:
|
||||
yarn start
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## Production
|
||||
|
||||
```sh
|
||||
blitz build
|
||||
blitz start --production
|
||||
```
|
||||
@@ -1,50 +0,0 @@
|
||||
import React from "react"
|
||||
import { AuthenticationError, Link, useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import login from "app/auth/mutations/login"
|
||||
import { LoginInput } from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [loginMutation] = useMutation(login)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<Form
|
||||
submitText="Login"
|
||||
schema={LoginInput}
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await loginMutation(values)
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
|
||||
} else {
|
||||
return {
|
||||
[FORM_ERROR]:
|
||||
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
|
||||
</Form>
|
||||
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
Or <Link href="/signup">Sign Up</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
@@ -1,44 +0,0 @@
|
||||
import React from "react"
|
||||
import { useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
import { SignupInput } 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={SignupInput}
|
||||
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,15 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
import { authenticateUser } from "app/auth/auth-utils"
|
||||
import { LoginInput, LoginInputType } from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, { session }: Ctx) {
|
||||
// This throws an error if input is invalid
|
||||
const { email, password } = LoginInput.parse(input)
|
||||
|
||||
// This throws an error if credentials are invalid
|
||||
const user = await authenticateUser(email, password)
|
||||
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
|
||||
export default async function logout(_: any, { session }: Ctx) {
|
||||
return await session.revoke()
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
import db from "db"
|
||||
import { hashPassword } from "app/auth/auth-utils"
|
||||
import { SignupInput, SignupInputType } from "app/auth/validations"
|
||||
|
||||
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: email.toLowerCase(), hashedPassword, role: "user" },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
})
|
||||
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from "react"
|
||||
import { useRouter, BlitzPage } from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import { LoginForm } from "app/auth/components/LoginForm"
|
||||
|
||||
const LoginPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LoginForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
|
||||
|
||||
export default LoginPage
|
||||
@@ -1,18 +0,0 @@
|
||||
import React from "react"
|
||||
import { useRouter, BlitzPage } from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import { SignupForm } from "app/auth/components/SignupForm"
|
||||
|
||||
const SignupPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SignupForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
SignupPage.getLayout = (page) => <Layout title="Sign Up">{page}</Layout>
|
||||
|
||||
export default SignupPage
|
||||
@@ -1,62 +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"
|
||||
|
||||
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
|
||||
onSubmit: FinalFormProps<z.infer<S>>["onSubmit"]
|
||||
initialValues?: FinalFormProps<z.infer<S>>["initialValues"]
|
||||
} & Omit<PropsWithoutRef<JSX.IntrinsicElements["form"]>, "onSubmit">
|
||||
|
||||
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>
|
||||
)}
|
||||
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitText}
|
||||
</button>
|
||||
|
||||
<style global jsx>{`
|
||||
.form > * + * {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`}</style>
|
||||
</form>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default Form
|
||||
@@ -1,57 +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)
|
||||
|
||||
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,22 +0,0 @@
|
||||
import { ReactNode } from "react"
|
||||
import { Head } from "blitz"
|
||||
|
||||
type LayoutProps = {
|
||||
title?: string
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const Layout = ({ title, children }: LayoutProps) => {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{title || "custom-server"}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
{children}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Layout
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Head, ErrorComponent } from "blitz"
|
||||
|
||||
// ------------------------------------------------------
|
||||
// This page is rendered if a route match is not found
|
||||
// ------------------------------------------------------
|
||||
export default function Page404() {
|
||||
const statusCode = 404
|
||||
const title = "This page could not be found"
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>
|
||||
{statusCode}: {title}
|
||||
</title>
|
||||
</Head>
|
||||
<ErrorComponent statusCode={statusCode} title={title} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
import { AppProps, ErrorComponent, useRouter, AuthenticationError, AuthorizationError } from "blitz"
|
||||
import { ErrorBoundary, FallbackProps } 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()
|
||||
|
||||
return (
|
||||
<ErrorBoundary
|
||||
FallbackComponent={RootErrorFallback}
|
||||
resetKeys={[router.asPath]}
|
||||
onReset={() => {
|
||||
// This ensures the Blitz useQuery hooks will automatically refetch
|
||||
// data any time you reset the error boundary
|
||||
queryCache.resetErrorBoundaries()
|
||||
}}
|
||||
>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
</ErrorBoundary>
|
||||
)
|
||||
}
|
||||
|
||||
function RootErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
|
||||
if (error instanceof AuthenticationError) {
|
||||
return <LoginForm onSuccess={resetErrorBoundary} />
|
||||
} else if (error instanceof AuthorizationError) {
|
||||
return (
|
||||
<ErrorComponent
|
||||
statusCode={(error as any).statusCode}
|
||||
title="Sorry, you are not authorized to access this"
|
||||
/>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<ErrorComponent
|
||||
statusCode={(error as any)?.statusCode || 400}
|
||||
title={error?.message || error?.name}
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import React from "react"
|
||||
import { render } from "test/utils"
|
||||
|
||||
import Home from "./index"
|
||||
import { useCurrentUser } from "app/hooks/useCurrentUser"
|
||||
|
||||
jest.mock("app/hooks/useCurrentUser")
|
||||
const mockUseCurrentUser = useCurrentUser as jest.MockedFunction<typeof useCurrentUser>
|
||||
|
||||
test.skip("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 linkElement = getByText(/Documentation/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
})
|
||||
@@ -1,272 +0,0 @@
|
||||
import {Link, BlitzPage, useMutation} from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import logout from "app/auth/mutations/logout"
|
||||
import {useCurrentUser} from "app/hooks/useCurrentUser"
|
||||
import {Suspense} from "react"
|
||||
|
||||
/*
|
||||
* This file is just for a pleasant getting started page for your new app.
|
||||
* You can delete everything in here and start from scratch if you like.
|
||||
*/
|
||||
|
||||
const UserInfo = () => {
|
||||
const currentUser = useCurrentUser()
|
||||
const [logoutMutation] = useMutation(logout)
|
||||
|
||||
if (currentUser) {
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
className="button small"
|
||||
onClick={async () => {
|
||||
await logoutMutation()
|
||||
}}
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<div>
|
||||
User id: <code>{currentUser.id}</code>
|
||||
<br />
|
||||
User role: <code>{currentUser.role}</code>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<Link href="/signup">
|
||||
<a className="button small">
|
||||
<strong>Sign Up</strong>
|
||||
</a>
|
||||
</Link>
|
||||
<Link href="/login">
|
||||
<a className="button small">
|
||||
<strong>Login</strong>
|
||||
</a>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const Home: BlitzPage = () => {
|
||||
return (
|
||||
<div className="container">
|
||||
<main>
|
||||
<div className="logo">
|
||||
<img src="/logo.png" alt="blitz.js" />
|
||||
</div>
|
||||
<p>
|
||||
<strong>Congrats!</strong> Your app is ready, including user sign-up and log-in.
|
||||
</p>
|
||||
<div className="buttons" style={{marginTop: "1rem", marginBottom: "1rem"}}>
|
||||
<Suspense fallback="Loading...">
|
||||
<UserInfo />
|
||||
</Suspense>
|
||||
</div>
|
||||
<p>
|
||||
<strong>
|
||||
To add a new model to your app, <br />
|
||||
run the following in your terminal:
|
||||
</strong>
|
||||
</p>
|
||||
<pre>
|
||||
<code>blitz generate all project name:string</code>
|
||||
</pre>
|
||||
<pre>
|
||||
<code>blitz prisma migrate dev --preview-feature</code>
|
||||
</pre>
|
||||
<div>
|
||||
<p>
|
||||
Then <strong>restart the server</strong>
|
||||
</p>
|
||||
<pre>
|
||||
<code>Ctrl + c</code>
|
||||
</pre>
|
||||
<pre>
|
||||
<code>blitz start</code>
|
||||
</pre>
|
||||
<p>
|
||||
and go to{" "}
|
||||
<Link href="/projects">
|
||||
<a>/projects</a>
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<div className="buttons" style={{marginTop: "5rem"}}>
|
||||
<a
|
||||
className="button"
|
||||
href="https://blitzjs.com/docs/getting-started?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<a
|
||||
className="button-outline"
|
||||
href="https://github.com/blitz-js/blitz"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Github Repo
|
||||
</a>
|
||||
<a
|
||||
className="button-outline"
|
||||
href="https://slack.blitzjs.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Slack Community
|
||||
</a>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
<a
|
||||
href="https://blitzjs.com?utm_source=blitz-new&utm_medium=app-template&utm_campaign=blitz-new"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Powered by Blitz.js
|
||||
</a>
|
||||
</footer>
|
||||
|
||||
<style jsx global>{`
|
||||
@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght@300;700&display=swap");
|
||||
|
||||
html,
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Libre Franklin", -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.container {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 5rem 0;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main p {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
footer {
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
border-top: 1px solid #eaeaea;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background-color: #45009d;
|
||||
}
|
||||
|
||||
footer a {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: #f4f4f4;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
grid-gap: 0.5rem;
|
||||
}
|
||||
.button {
|
||||
font-size: 1rem;
|
||||
background-color: #6700eb;
|
||||
padding: 1rem 2rem;
|
||||
color: #f4f4f4;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button.small {
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #45009d;
|
||||
}
|
||||
|
||||
.button-outline {
|
||||
border: 2px solid #6700eb;
|
||||
padding: 1rem 2rem;
|
||||
color: #6700eb;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.button-outline:hover {
|
||||
border-color: #45009d;
|
||||
color: #45009d;
|
||||
}
|
||||
|
||||
pre {
|
||||
background: #fafafa;
|
||||
border-radius: 5px;
|
||||
padding: 0.75rem;
|
||||
text-align: center;
|
||||
}
|
||||
code {
|
||||
font-size: 0.9rem;
|
||||
font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
|
||||
Bitstream Vera Sans Mono, Courier New, monospace;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
max-width: 800px;
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.grid {
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Home.getLayout = (page) => <Layout title="Home">{page}</Layout>
|
||||
|
||||
export default Home
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
import db from "db"
|
||||
|
||||
export default async function getCurrentUser(_ = null, { session }: Ctx) {
|
||||
if (!session.userId) return null
|
||||
|
||||
const user = await db.user.findOne({
|
||||
where: { id: session.userId },
|
||||
select: { id: true, name: true, email: true, role: true },
|
||||
})
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
const { sessionMiddleware, simpleRolesIsAuthorized } = require("@blitzjs/server")
|
||||
|
||||
module.exports = {
|
||||
middleware: [
|
||||
sessionMiddleware({
|
||||
isAuthorized: simpleRolesIsAuthorized,
|
||||
}),
|
||||
],
|
||||
/* Uncomment this to customize the webpack config
|
||||
webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
|
||||
// Note: we provide webpack above so you should not `require` it
|
||||
// Perform customizations to webpack config
|
||||
// Important: return the modified config
|
||||
return config
|
||||
},
|
||||
*/
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://localhost:3099",
|
||||
"defaultCommandTimeout": 10000,
|
||||
"video": false,
|
||||
"chromeWebSecurity": false
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
8
examples/custom-server/cypress/index.d.ts
vendored
8
examples/custom-server/cypress/index.d.ts
vendored
@@ -1,8 +0,0 @@
|
||||
/// <reference types="Cypress" />
|
||||
/// <reference types="@cypress/skip-test" />
|
||||
|
||||
declare namespace Cypress {
|
||||
interface Chainable {
|
||||
signup(user: { email: string; password: string }): void
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { createRandomUser } from "../support/helpers"
|
||||
|
||||
describe("index page", () => {
|
||||
beforeEach(() => {
|
||||
cy.visit("/")
|
||||
})
|
||||
|
||||
it("goes to the signup page", () => {
|
||||
cy.contains("a", "Sign Up").click()
|
||||
cy.location("pathname").should("equal", "/signup")
|
||||
})
|
||||
|
||||
it("goes to the login page", () => {
|
||||
cy.contains("a", /login/i).click()
|
||||
cy.location("pathname").should("equal", "/login")
|
||||
})
|
||||
|
||||
it("allows the user to signup", () => {
|
||||
const user = createRandomUser()
|
||||
|
||||
cy.signup(user)
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.contains("button", "Logout")
|
||||
})
|
||||
|
||||
it("allows the user to log in", () => {
|
||||
const user = createRandomUser()
|
||||
|
||||
cy.signup(user)
|
||||
|
||||
cy.contains("button", "Logout").click()
|
||||
cy.contains("a", /login/i).click()
|
||||
|
||||
cy.contains("Email").find("input").type(user.email)
|
||||
cy.contains("Password").find("input").type(user.password)
|
||||
cy.contains("button", /login/i).click()
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.contains("button", "Logout")
|
||||
})
|
||||
|
||||
it("allows the user to logout", () => {
|
||||
const user = createRandomUser()
|
||||
|
||||
cy.signup(user)
|
||||
|
||||
cy.contains("button", "Logout").click()
|
||||
|
||||
cy.location("pathname").should("equal", "/")
|
||||
cy.contains("a", /login/i)
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
||||
@@ -1,22 +0,0 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
//@ts-ignore
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
|
||||
Cypress.Commands.add("signup", ({ email, password }) => {
|
||||
cy.contains("a", "Sign Up").click()
|
||||
|
||||
cy.contains("Email").find("input").type(email)
|
||||
cy.contains("Password").find("input").type(password)
|
||||
cy.contains("button", "Create Account").click()
|
||||
})
|
||||
@@ -1,7 +0,0 @@
|
||||
export const createRandomUser = () => {
|
||||
const random = Math.round(Math.random() * 100000).toString()
|
||||
const email = `test_${random}@example.com`
|
||||
const password = `password_${random}`
|
||||
|
||||
return { email, password }
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import "./commands"
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
||||
Cypress.Screenshot.defaults({
|
||||
screenshotOnRunFailure: false,
|
||||
})
|
||||
|
||||
require("@cypress/skip-test/support")
|
||||
@@ -1,7 +0,0 @@
|
||||
import {makeServerOnlyPrisma} from "blitz"
|
||||
import {PrismaClient} from "@prisma/client"
|
||||
|
||||
const ServerOnlyPrisma = makeServerOnlyPrisma(PrismaClient)
|
||||
|
||||
export * from "@prisma/client"
|
||||
export default new ServerOnlyPrisma()
|
||||
@@ -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,38 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
url = "file:./db.sqlite"
|
||||
}
|
||||
|
||||
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?
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
// import db from "./index"
|
||||
|
||||
/*
|
||||
* This seed function is executed when you run `blitz db seed`.
|
||||
*
|
||||
* Probably you want to use a library like https://chancejs.com
|
||||
* or https://github.com/Marak/Faker.js to easily generate
|
||||
* realistic data.
|
||||
*/
|
||||
const seed = async () => {
|
||||
// for (let i = 0; i < 5; i++) {
|
||||
// await db.project.create({ data: { name: "Project " + i } })
|
||||
// }
|
||||
}
|
||||
|
||||
export default seed
|
||||
@@ -1,3 +0,0 @@
|
||||
module.exports = {
|
||||
preset: "blitz",
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"name": "@examples/custom-server",
|
||||
"version": "0.29.2",
|
||||
"scripts": {
|
||||
"start": "nodemon --watch server.js --exec 'blitz start'",
|
||||
"build": "blitz build",
|
||||
"studio": "blitz prisma studio",
|
||||
"lint": "eslint --ignore-path .gitignore --ext .js,.ts,.tsx .",
|
||||
"test-watch": "jest --watch",
|
||||
"cy-open": "cypress open",
|
||||
"cy-run": "cypress run",
|
||||
"test:migrate": "prisma generate && blitz prisma migrate deploy --preview-feature",
|
||||
"test:jest": "jest",
|
||||
"test-server": "blitz build && blitz start --production",
|
||||
"test:e2e": "cross-env NODE_ENV=test PORT=3099 start-server-and-test test-server http://localhost:3099 cy-run",
|
||||
"test": "run-s test:*"
|
||||
},
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
"prisma": {
|
||||
"schema": "db/schema.prisma"
|
||||
},
|
||||
"prettier": {
|
||||
"semi": false,
|
||||
"printWidth": 100,
|
||||
"bracketSpacing": false,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "lint-staged && pretty-quick --staged",
|
||||
"pre-push": "tsc && npm run lint && npm run test"
|
||||
}
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{js,ts,tsx}": [
|
||||
"eslint --fix"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@prisma/cli": "2.14.0",
|
||||
"@prisma/client": "2.14.0",
|
||||
"blitz": "0.29.2",
|
||||
"final-form": "4.20.1",
|
||||
"react": "0.0.0-experimental-4ead6b530",
|
||||
"react-dom": "0.0.0-experimental-4ead6b530",
|
||||
"react-error-boundary": "2.3.2",
|
||||
"react-final-form": "6.5.2",
|
||||
"secure-password": "4.0.0",
|
||||
"typescript": "4.1.3",
|
||||
"zod": "1.11.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cypress/skip-test": "2.5.0",
|
||||
"@testing-library/jest-dom": "5.11.6",
|
||||
"@testing-library/react": "11.2.3",
|
||||
"@testing-library/react-hooks": "4.0.1",
|
||||
"@types/jest": "26.0.20",
|
||||
"@types/react": "16.14.1",
|
||||
"@types/secure-password": "3.1.0",
|
||||
"@typescript-eslint/eslint-plugin": "4.12.0",
|
||||
"@typescript-eslint/parser": "4.12.0",
|
||||
"babel-eslint": "10.1.0",
|
||||
"cypress": "6.2.1",
|
||||
"eslint": "7.17.0",
|
||||
"eslint-config-react-app": "5.2.1",
|
||||
"eslint-plugin-cypress": "2.11.1",
|
||||
"eslint-plugin-flowtype": "5.2.0",
|
||||
"eslint-plugin-import": "2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "6.4.1",
|
||||
"eslint-plugin-react": "7.21.5",
|
||||
"eslint-plugin-react-hooks": "4.2.0",
|
||||
"husky": "4.3.7",
|
||||
"jest": "26.6.3",
|
||||
"jest-environment-jsdom-fourteen": "1.0.1",
|
||||
"jest-watch-typeahead": "0.6.1",
|
||||
"lint-staged": "10.5.1",
|
||||
"nodemon": "2.0.7",
|
||||
"npm-run-all": "4.1.5",
|
||||
"prettier": "2.2.0",
|
||||
"pretty-quick": "3.1.0",
|
||||
"react-test-renderer": "16.14.0",
|
||||
"start-server-and-test": "1.11.2",
|
||||
"ts-jest": "26.4.4"
|
||||
},
|
||||
"private": true
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 33 KiB |
@@ -1,25 +0,0 @@
|
||||
const { createServer } = require("http")
|
||||
const { parse } = require("url")
|
||||
const blitz = require("@blitzjs/server")
|
||||
const { log } = require("@blitzjs/display")
|
||||
|
||||
const { PORT = "3000" } = process.env
|
||||
const dev = process.env.NODE_ENV !== "production"
|
||||
const app = blitz({ dev })
|
||||
const handle = app.getRequestHandler()
|
||||
|
||||
app.prepare().then(() => {
|
||||
createServer((req, res) => {
|
||||
const parsedUrl = parse(req.url, true)
|
||||
const { pathname } = parsedUrl
|
||||
|
||||
if (pathname === "/hello") {
|
||||
res.writeHead(200).end("world")
|
||||
return
|
||||
}
|
||||
|
||||
handle(req, res, parsedUrl)
|
||||
}).listen(PORT, () => {
|
||||
log.success(`Ready on http://localhost:${PORT}`)
|
||||
})
|
||||
})
|
||||
@@ -1,2 +0,0 @@
|
||||
// This is the jest 'setupFilesAfterEnv' setup file
|
||||
// It's a good place to set globals, add global before/after hooks, etc
|
||||
@@ -1,94 +0,0 @@
|
||||
import React from "react"
|
||||
import { RouterContext, BlitzRouter } from "blitz"
|
||||
import { render as defaultRender } from "@testing-library/react"
|
||||
import { renderHook as defaultRenderHook } from "@testing-library/react-hooks"
|
||||
|
||||
export * from "@testing-library/react"
|
||||
|
||||
// --------------------------------------------------------------------------------
|
||||
// This file customizes the render() and renderHook() test functions provided
|
||||
// by React testing library. It adds a router context wrapper with a mocked router.
|
||||
//
|
||||
// You should always import `render` and `renderHook` from this file
|
||||
//
|
||||
// This is the place to add any other context providers you need while testing.
|
||||
// --------------------------------------------------------------------------------
|
||||
|
||||
// --------------------------------------------------
|
||||
// render()
|
||||
// --------------------------------------------------
|
||||
// Override the default test render with our own
|
||||
//
|
||||
// You can override the router mock like this:
|
||||
//
|
||||
// const { baseElement } = render(<MyComponent />, {
|
||||
// router: { pathname: '/my-custom-pathname' },
|
||||
// });
|
||||
// --------------------------------------------------
|
||||
export function render(ui: RenderUI, { wrapper, router, ...options }: RenderOptions = {}) {
|
||||
if (!wrapper) {
|
||||
// Add a default context wrapper if one isn't supplied from the test
|
||||
wrapper = ({ children }) => (
|
||||
<RouterContext.Provider value={{ ...mockRouter, ...router }}>
|
||||
{children}
|
||||
</RouterContext.Provider>
|
||||
)
|
||||
}
|
||||
return defaultRender(ui, { wrapper, ...options })
|
||||
}
|
||||
|
||||
// --------------------------------------------------
|
||||
// renderHook()
|
||||
// --------------------------------------------------
|
||||
// Override the default test renderHook with our own
|
||||
//
|
||||
// You can override the router mock like this:
|
||||
//
|
||||
// const result = renderHook(() => myHook(), {
|
||||
// router: { pathname: '/my-custom-pathname' },
|
||||
// });
|
||||
// --------------------------------------------------
|
||||
export function renderHook(
|
||||
hook: RenderHook,
|
||||
{ wrapper, router, ...options }: RenderHookOptions = {}
|
||||
) {
|
||||
if (!wrapper) {
|
||||
// Add a default context wrapper if one isn't supplied from the test
|
||||
wrapper = ({ children }) => (
|
||||
<RouterContext.Provider value={{ ...mockRouter, ...router }}>
|
||||
{children}
|
||||
</RouterContext.Provider>
|
||||
)
|
||||
}
|
||||
return defaultRenderHook(hook, { wrapper, ...options })
|
||||
}
|
||||
|
||||
export const mockRouter: BlitzRouter = {
|
||||
basePath: "",
|
||||
pathname: "/",
|
||||
route: "/",
|
||||
asPath: "/",
|
||||
params: {},
|
||||
query: {},
|
||||
isReady: true,
|
||||
push: jest.fn(),
|
||||
replace: jest.fn(),
|
||||
reload: jest.fn(),
|
||||
back: jest.fn(),
|
||||
prefetch: jest.fn(),
|
||||
beforePopState: jest.fn(),
|
||||
events: {
|
||||
on: jest.fn(),
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
},
|
||||
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,23 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"baseUrl": "./",
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": false,
|
||||
"strictNullChecks": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"tsBuildInfoFile": ".tsbuildinfo"
|
||||
},
|
||||
"exclude": ["node_modules"],
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { DefaultCtx, SessionContext, DefaultPublicData } from "blitz"
|
||||
import { User } from "db"
|
||||
import React from "react"
|
||||
|
||||
declare module "blitz" {
|
||||
export interface Ctx extends DefaultCtx {
|
||||
session: SessionContext
|
||||
}
|
||||
export interface PublicData extends DefaultPublicData {
|
||||
userId: User["id"]
|
||||
}
|
||||
}
|
||||
|
||||
// This should not be needed. Usually it isn't but for some reason in this example it is
|
||||
declare module "react" {
|
||||
interface StyleHTMLAttributes<T> extends React.HTMLAttributes<T> {
|
||||
jsx?: boolean
|
||||
global?: boolean
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
save-exact=true
|
||||
@@ -1,37 +0,0 @@
|
||||
# Blitz Fauna Example
|
||||
|
||||
## Intro
|
||||
|
||||
This example shows how to use [Fauna](https://dashboard.fauna.com/accounts/register?utm_source=BlitzJS&utm_medium=sponsorship&utm_campaign=BlitzJS_Sponsorship_2020) instead of Prisma and Postgres.
|
||||
|
||||
The bulk of the integration is in the following files:
|
||||
|
||||
- `blitz.config.js`
|
||||
- `db/index.ts`
|
||||
|
||||
And then also the queries and mutations use Fauna.
|
||||
|
||||
By far the main integration work is providing the auth session hooks for reading and writing session data to Fauna. All this is in `blitz.config.js`.
|
||||
|
||||
This example use the Fauna GraphQL API since it's more familiar to most people than FQL.
|
||||
|
||||
## Getting Started
|
||||
|
||||
1. Sign up for a Fauna account
|
||||
1. Create a new database
|
||||
1. Click on the GraphQL menu item
|
||||
1. Upload the graphql schema located at `db/schema.graphql`
|
||||
1. Click on the Security menu item
|
||||
1. Create a new auth key, and add the auth key to `.env.local` like this:
|
||||
|
||||
```
|
||||
FAUNA_SECRET=YOUR_AUTH_KEY
|
||||
```
|
||||
|
||||
2. Start the dev server
|
||||
|
||||
```
|
||||
yarn blitz start
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
@@ -1,47 +0,0 @@
|
||||
import { SecurePassword, AuthenticationError } from "blitz"
|
||||
import db from "db"
|
||||
import { gql } from "graphql-request"
|
||||
|
||||
export const authenticateUser = async (email: string, password: string) => {
|
||||
const { user } = await db.request(
|
||||
gql`
|
||||
query getUser($email: String!) {
|
||||
user: findUserByEmail(email: $email) {
|
||||
id: _id
|
||||
email
|
||||
name
|
||||
role
|
||||
hashedPassword
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ email: email.toLowerCase() }
|
||||
)
|
||||
|
||||
if (!user || !user.hashedPassword) throw new AuthenticationError()
|
||||
|
||||
const result = await SecurePassword.verify(user.hashedPassword, password)
|
||||
|
||||
if (result === SecurePassword.VALID_NEEDS_REHASH) {
|
||||
// Upgrade hashed password with a more secure hash
|
||||
const improvedHash = await SecurePassword.hash(password)
|
||||
await db.request(
|
||||
gql`
|
||||
mutation UpdateUser($data: UserInput!) {
|
||||
updateUser(data: $data) {
|
||||
id: _id
|
||||
}
|
||||
}
|
||||
`,
|
||||
{
|
||||
data: {
|
||||
id: user.id,
|
||||
hashedPassword: improvedHash,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const { hashedPassword, ...rest } = user
|
||||
return rest
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { Link, useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import login from "app/auth/mutations/login"
|
||||
import { LoginInput } from "app/auth/validations"
|
||||
|
||||
type LoginFormProps = {
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export const LoginForm = (props: LoginFormProps) => {
|
||||
const [loginMutation] = useMutation(login)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Login</h1>
|
||||
|
||||
<Form
|
||||
submitText="Log In"
|
||||
schema={LoginInput}
|
||||
initialValues={{ email: "", password: "" }}
|
||||
onSubmit={async (values) => {
|
||||
try {
|
||||
await loginMutation(values)
|
||||
props.onSuccess?.()
|
||||
} catch (error) {
|
||||
if (error.name === "AuthenticationError") {
|
||||
return { [FORM_ERROR]: "Sorry, those credentials are invalid" }
|
||||
} else {
|
||||
return {
|
||||
[FORM_ERROR]:
|
||||
"Sorry, we had an unexpected error. Please try again. - " + error.toString(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<LabeledTextField name="email" label="Email" placeholder="Email" />
|
||||
<LabeledTextField name="password" label="Password" placeholder="Password" type="password" />
|
||||
</Form>
|
||||
|
||||
<div style={{ marginTop: "1rem" }}>
|
||||
Or <Link href="/signup">Sign Up</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LoginForm
|
||||
@@ -1,43 +0,0 @@
|
||||
import { useMutation } from "blitz"
|
||||
import { LabeledTextField } from "app/components/LabeledTextField"
|
||||
import { Form, FORM_ERROR } from "app/components/Form"
|
||||
import signup from "app/auth/mutations/signup"
|
||||
import { SignupInput } 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={SignupInput}
|
||||
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,15 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
import { authenticateUser } from "app/auth/auth-utils"
|
||||
import { LoginInput, LoginInputType } from "../validations"
|
||||
|
||||
export default async function login(input: LoginInputType, { session }: Ctx) {
|
||||
// This throws an error if input is invalid
|
||||
const { email, password } = LoginInput.parse(input)
|
||||
|
||||
// This throws an error if credentials are invalid
|
||||
const user = await authenticateUser(email, password)
|
||||
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { Ctx } from "blitz"
|
||||
|
||||
export default async function logout(_: any, { session }: Ctx) {
|
||||
return await session.revoke()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Ctx, SecurePassword } from "blitz"
|
||||
import db from "db"
|
||||
import { SignupInput, SignupInputType } from "app/auth/validations"
|
||||
import { gql } from "graphql-request"
|
||||
|
||||
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 SecurePassword.hash(password)
|
||||
const { user } = await db.request(
|
||||
gql`
|
||||
mutation createUser($email: String!, $hashedPassword: String, $role: String!) {
|
||||
user: createUser(data: { email: $email, hashedPassword: $hashedPassword, role: $role }) {
|
||||
id: _id
|
||||
email
|
||||
name
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
{ email: email.toLowerCase(), hashedPassword, role: "user" }
|
||||
)
|
||||
console.log("Create user result:", user)
|
||||
|
||||
await session.create({ userId: user.id, roles: [user.role] })
|
||||
|
||||
return user
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { useRouter, BlitzPage } from "blitz"
|
||||
import Layout from "app/layouts/Layout"
|
||||
import { LoginForm } from "app/auth/components/LoginForm"
|
||||
|
||||
const LoginPage: BlitzPage = () => {
|
||||
const router = useRouter()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<LoginForm onSuccess={() => router.push("/")} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
LoginPage.getLayout = (page) => <Layout title="Log In">{page}</Layout>
|
||||
|
||||
export default LoginPage
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user