├── .env
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── docker-compose.yml
├── next-env.d.ts
├── package.json
├── prisma
├── .env
├── migrations
│ ├── 20201110101443
│ │ ├── README.md
│ │ ├── schema.prisma
│ │ └── steps.json
│ └── migrate.lock
└── schema.prisma
├── public
├── favicon.ico
└── vercel.svg
├── src
├── components
│ ├── Navbar.tsx
│ ├── forms
│ │ ├── CreateUserForm.tsx
│ │ └── LoginForm.tsx
│ └── layouts
│ │ └── AuthLayout.tsx
├── constants.ts
├── interfaces
│ └── user.ts
├── pages
│ ├── _app.tsx
│ ├── api
│ │ ├── auth
│ │ │ ├── login.ts
│ │ │ ├── logout.ts
│ │ │ └── me.ts
│ │ ├── socketio.ts
│ │ └── user
│ │ │ ├── create.ts
│ │ │ └── get.ts
│ └── index.tsx
├── styles
│ ├── Home.module.css
│ └── globals.css
├── types
│ ├── next.ts
│ └── user.ts
└── utils
│ ├── cache.ts
│ ├── cookie.ts
│ ├── format.ts
│ ├── hooks
│ └── useQuerySocket.ts
│ ├── http.ts
│ ├── validation.ts
│ └── validators
│ └── user.ts
├── tsconfig.json
└── yarn.lock
/.env:
--------------------------------------------------------------------------------
1 | # POSTGRES DB
2 | POSTGRES_USER="postgres"
3 | POSTGRES_PASSWORD="postgres"
4 | POSTGRES_DB="typegraphql-example"
5 |
6 | # PRISMA CONNECTION
7 | DATABASE_URL="postgresql://postgres:postgres@localhost:5432/typegraphql-example?schema=public"
8 |
9 | # JWT
10 | JWT_AUTH_SECRET = "oiNOI9bW7876tr5r5dfgjhbkhvcc5uio"
11 | JWT_REFRESH_SECRET = "8olkjfq44lwke4fj83lbkj5h3j5kjf65"
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env.local
29 | .env.development.local
30 | .env.test.local
31 | .env.production.local
32 |
33 | # vercel
34 | .vercel
35 |
36 | #VS Code
37 | /.vscode
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "typescript.tsdk": "node_modules/typescript/lib"
3 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Query real-time refetch example
with Socket.io (v3) and Next.js API Routes (v10.0.x)
2 |
3 | ## NEW UPDATE
4 |
5 | This boilerplate now implements **Socket.io** in **Next.js API routes** to give RT functionalities to **React Query**.
6 |
7 | - A custom **useQuerySocket** hook takes care to subscribe to a socket.io event name with the same value of the **cache key**.
8 | - **On the server**, notifications are emitted by the REST method when a new user has been created.
9 | - **On the client**, the hook is listening for any message emitted by the server, performing a new **refetch** for all the queries that are using the same cache key.
10 |
11 | **TRY IT IN ACTION:**
12 |
13 | 1. Open two different browser windows and register a new user in one of them.
14 |
15 | 2. The second browser window will refresh user's list in **real-time**
16 |
17 | ## What is it?
18 |
19 | This is an experimental full-stack boilerplate which puts together **Next.js** v10.0.1, **API routes** and **Prisma v2** as backend + **Yup** schema for server-side validation and **Chakra UI** + **react-query** on the frontend.
20 |
21 | Prisma v2 connects to a **PostgreSQL** database provided by the docker-compose file
22 |
23 | ## Getting Started
24 |
25 | Clone this repo and take care to have **docker-compose** installed and **Docker** daemon up and running in your DEV machine.
26 |
27 | Then:
28 |
29 | ```bash
30 | # build and run Postgres db
31 | docker-compose up --build
32 |
33 | # install dependencies
34 | yarn install
35 |
36 | # migrate Prisma schema to db
37 | yarn prisma migrate up --experimental
38 |
39 | # generate Prisma client
40 | yarn prisma generate
41 |
42 | # run dev server
43 | yarn dev
44 | ```
45 |
46 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
47 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.6"
2 | services:
3 | # next-graphql:
4 | # container_name: next_graphql
5 | # depends_on:
6 | # - postgres
7 | # restart: always
8 | # build: .
9 | # ports:
10 | # - "3000:3000"
11 | # environment:
12 | # - DB_URL=mongodb://mongo_db
13 | # - GRAPHQL_URI=http://localhost:3000/api/graphql-data
14 | # volumes:
15 | # - .:/usr/src/app
16 | # - /usr/src/app/.next
17 | # - /usr/src/app/node_modules
18 | # redis:
19 | # image: redis:alpine
20 | # restart: always
21 | # command: redis-server --requirepass redis_password
22 | # volumes:
23 | # - redis_data:/var/lib/redis/data
24 | # # - redis_data/redis.conf:/var/lib/redis/data/redis.conf
25 | # environment:
26 | # REDIS_REPLICATION_MODE: master
27 | # ports:
28 | # - 6379:6379
29 | postgres:
30 | container_name: postgres
31 | image: postgres:12.4-alpine
32 | restart: always
33 | env_file:
34 | - .env
35 | volumes:
36 | - db_data:/var/lib/postgresql/data
37 | ports:
38 | - 5432:5432
39 | # networks:
40 | # - backend
41 | volumes:
42 | db_data:
43 | # redis_data:
44 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next10-api-routes-prisma",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start"
9 | },
10 | "dependencies": {
11 | "@chakra-ui/core": "^1.0.0-rc.8",
12 | "@hookform/resolvers": "^1.0.1",
13 | "@prisma/client": "^2.10.2",
14 | "bcryptjs": "^2.4.3",
15 | "cookie": "^0.4.1",
16 | "framer-motion": "^2.9.4",
17 | "jsonwebtoken": "^8.5.1",
18 | "lodash": "^4.17.20",
19 | "next": "10.0.1",
20 | "react": "17.0.1",
21 | "react-dom": "17.0.1",
22 | "react-hook-form": "^6.11.0",
23 | "react-query": "^2.26.2",
24 | "socket.io": "^3.0.1",
25 | "socket.io-client": "^3.0.1",
26 | "yup": "^0.29.3"
27 | },
28 | "devDependencies": {
29 | "@prisma/cli": "^2.10.2",
30 | "@types/bcryptjs": "^2.4.2",
31 | "@types/cookie": "^0.4.0",
32 | "@types/jsonwebtoken": "^8.5.0",
33 | "@types/lodash": "^4.14.165",
34 | "@types/node": "^14.14.7",
35 | "@types/react": "^16.9.56",
36 | "@types/yup": "^0.29.9",
37 | "typescript": "^4.0.5"
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/prisma/.env:
--------------------------------------------------------------------------------
1 | # Environment variables declared in this file are automatically made available to Prisma.
2 | # See the documentation for more detail: https://pris.ly/d/prisma-schema#using-environment-variables
3 |
4 | # Prisma supports the native connection string format for PostgreSQL, MySQL and SQLite.
5 | # See the documentation for all the connection string options: https://pris.ly/d/connection-strings
6 |
7 | # DATABASE_URL="postgresql://postgres:postgres@localhost:5432/typegraphql-example?schema=public"
--------------------------------------------------------------------------------
/prisma/migrations/20201110101443/README.md:
--------------------------------------------------------------------------------
1 | # Migration `20201110101443`
2 |
3 | This migration has been generated by redbaron76 at 11/10/2020, 11:14:43 AM.
4 | You can check out the [state of the schema](./schema.prisma) after the migration.
5 |
6 | ## Database Steps
7 |
8 | ```sql
9 | CREATE TYPE "public"."Role" AS ENUM ('USER', 'PLAYER', 'SHOP', 'ADMIN')
10 |
11 | CREATE TABLE "public"."User" (
12 | "id" SERIAL,
13 | "email" text NOT NULL ,
14 | "firstName" text NOT NULL ,
15 | "lastName" text NOT NULL ,
16 | "password" text NOT NULL ,
17 | "role" "Role" NOT NULL DEFAULT E'USER',
18 | "count" integer NOT NULL DEFAULT 0,
19 | PRIMARY KEY ("id")
20 | )
21 |
22 | CREATE TABLE "public"."Profile" (
23 | "id" SERIAL,
24 | "bio" text NOT NULL ,
25 | "userId" integer NOT NULL ,
26 | PRIMARY KEY ("id")
27 | )
28 |
29 | CREATE TABLE "public"."Book" (
30 | "id" SERIAL,
31 | "title" text NOT NULL ,
32 | "userId" integer NOT NULL ,
33 | PRIMARY KEY ("id")
34 | )
35 |
36 | CREATE TABLE "public"."Category" (
37 | "id" SERIAL,
38 | PRIMARY KEY ("id")
39 | )
40 |
41 | CREATE TABLE "public"."_BookToCategory" (
42 | "A" integer NOT NULL ,
43 | "B" integer NOT NULL
44 | )
45 |
46 | CREATE UNIQUE INDEX "User.email_unique" ON "public"."User"("email")
47 |
48 | CREATE UNIQUE INDEX "Profile_userId_unique" ON "public"."Profile"("userId")
49 |
50 | CREATE UNIQUE INDEX "_BookToCategory_AB_unique" ON "public"."_BookToCategory"("A", "B")
51 |
52 | CREATE INDEX "_BookToCategory_B_index" ON "public"."_BookToCategory"("B")
53 |
54 | ALTER TABLE "public"."Profile" ADD FOREIGN KEY("userId")REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE
55 |
56 | ALTER TABLE "public"."Book" ADD FOREIGN KEY("userId")REFERENCES "public"."User"("id") ON DELETE CASCADE ON UPDATE CASCADE
57 |
58 | ALTER TABLE "public"."_BookToCategory" ADD FOREIGN KEY("A")REFERENCES "public"."Book"("id") ON DELETE CASCADE ON UPDATE CASCADE
59 |
60 | ALTER TABLE "public"."_BookToCategory" ADD FOREIGN KEY("B")REFERENCES "public"."Category"("id") ON DELETE CASCADE ON UPDATE CASCADE
61 | ```
62 |
63 | ## Changes
64 |
65 | ```diff
66 | diff --git schema.prisma schema.prisma
67 | migration ..20201110101443
68 | --- datamodel.dml
69 | +++ datamodel.dml
70 | @@ -1,0 +1,50 @@
71 | +// This is your Prisma schema file,
72 | +// learn more about it in the docs: https://pris.ly/d/prisma-schema
73 | +
74 | +datasource db {
75 | + provider = "postgresql"
76 | + url = "***"
77 | +}
78 | +
79 | +generator client {
80 | + provider = "prisma-client-js"
81 | +}
82 | +
83 | +enum Role {
84 | + USER
85 | + PLAYER
86 | + SHOP
87 | + ADMIN
88 | +}
89 | +
90 | +model User {
91 | + id Int @id @default(autoincrement())
92 | + email String @unique
93 | + firstName String
94 | + lastName String
95 | + password String
96 | + role Role @default(USER)
97 | + count Int @default(0)
98 | + books Book[]
99 | + profile Profile
100 | +}
101 | +
102 | +model Profile {
103 | + id Int @id @default(autoincrement())
104 | + bio String
105 | + user User @relation(fields: [userId], references: [id])
106 | + userId Int
107 | +}
108 | +
109 | +model Book {
110 | + id Int @id @default(autoincrement())
111 | + title String
112 | + author User @relation(fields: [userId], references: [id])
113 | + userId Int
114 | + categories Category[]
115 | +}
116 | +
117 | +model Category {
118 | + id Int @id @default(autoincrement())
119 | + books Book[]
120 | +}
121 | ```
122 |
123 |
124 |
--------------------------------------------------------------------------------
/prisma/migrations/20201110101443/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | datasource db {
5 | provider = "postgresql"
6 | url = "***"
7 | }
8 |
9 | generator client {
10 | provider = "prisma-client-js"
11 | }
12 |
13 | enum Role {
14 | USER
15 | PLAYER
16 | SHOP
17 | ADMIN
18 | }
19 |
20 | model User {
21 | id Int @id @default(autoincrement())
22 | email String @unique
23 | firstName String
24 | lastName String
25 | password String
26 | role Role @default(USER)
27 | count Int @default(0)
28 | books Book[]
29 | profile Profile
30 | }
31 |
32 | model Profile {
33 | id Int @id @default(autoincrement())
34 | bio String
35 | user User @relation(fields: [userId], references: [id])
36 | userId Int
37 | }
38 |
39 | model Book {
40 | id Int @id @default(autoincrement())
41 | title String
42 | author User @relation(fields: [userId], references: [id])
43 | userId Int
44 | categories Category[]
45 | }
46 |
47 | model Category {
48 | id Int @id @default(autoincrement())
49 | books Book[]
50 | }
51 |
--------------------------------------------------------------------------------
/prisma/migrations/20201110101443/steps.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.3.14-fixed",
3 | "steps": [
4 | {
5 | "tag": "CreateEnum",
6 | "enum": "Role",
7 | "values": [
8 | "USER",
9 | "PLAYER",
10 | "SHOP",
11 | "ADMIN"
12 | ]
13 | },
14 | {
15 | "tag": "CreateSource",
16 | "source": "db"
17 | },
18 | {
19 | "tag": "CreateArgument",
20 | "location": {
21 | "tag": "Source",
22 | "source": "db"
23 | },
24 | "argument": "provider",
25 | "value": "\"postgresql\""
26 | },
27 | {
28 | "tag": "CreateArgument",
29 | "location": {
30 | "tag": "Source",
31 | "source": "db"
32 | },
33 | "argument": "url",
34 | "value": "\"***\""
35 | },
36 | {
37 | "tag": "CreateModel",
38 | "model": "User"
39 | },
40 | {
41 | "tag": "CreateField",
42 | "model": "User",
43 | "field": "id",
44 | "type": "Int",
45 | "arity": "Required"
46 | },
47 | {
48 | "tag": "CreateDirective",
49 | "location": {
50 | "path": {
51 | "tag": "Field",
52 | "model": "User",
53 | "field": "id"
54 | },
55 | "directive": "id"
56 | }
57 | },
58 | {
59 | "tag": "CreateDirective",
60 | "location": {
61 | "path": {
62 | "tag": "Field",
63 | "model": "User",
64 | "field": "id"
65 | },
66 | "directive": "default"
67 | }
68 | },
69 | {
70 | "tag": "CreateArgument",
71 | "location": {
72 | "tag": "Directive",
73 | "path": {
74 | "tag": "Field",
75 | "model": "User",
76 | "field": "id"
77 | },
78 | "directive": "default"
79 | },
80 | "argument": "",
81 | "value": "autoincrement()"
82 | },
83 | {
84 | "tag": "CreateField",
85 | "model": "User",
86 | "field": "email",
87 | "type": "String",
88 | "arity": "Required"
89 | },
90 | {
91 | "tag": "CreateDirective",
92 | "location": {
93 | "path": {
94 | "tag": "Field",
95 | "model": "User",
96 | "field": "email"
97 | },
98 | "directive": "unique"
99 | }
100 | },
101 | {
102 | "tag": "CreateField",
103 | "model": "User",
104 | "field": "firstName",
105 | "type": "String",
106 | "arity": "Required"
107 | },
108 | {
109 | "tag": "CreateField",
110 | "model": "User",
111 | "field": "lastName",
112 | "type": "String",
113 | "arity": "Required"
114 | },
115 | {
116 | "tag": "CreateField",
117 | "model": "User",
118 | "field": "password",
119 | "type": "String",
120 | "arity": "Required"
121 | },
122 | {
123 | "tag": "CreateField",
124 | "model": "User",
125 | "field": "role",
126 | "type": "Role",
127 | "arity": "Required"
128 | },
129 | {
130 | "tag": "CreateDirective",
131 | "location": {
132 | "path": {
133 | "tag": "Field",
134 | "model": "User",
135 | "field": "role"
136 | },
137 | "directive": "default"
138 | }
139 | },
140 | {
141 | "tag": "CreateArgument",
142 | "location": {
143 | "tag": "Directive",
144 | "path": {
145 | "tag": "Field",
146 | "model": "User",
147 | "field": "role"
148 | },
149 | "directive": "default"
150 | },
151 | "argument": "",
152 | "value": "USER"
153 | },
154 | {
155 | "tag": "CreateField",
156 | "model": "User",
157 | "field": "count",
158 | "type": "Int",
159 | "arity": "Required"
160 | },
161 | {
162 | "tag": "CreateDirective",
163 | "location": {
164 | "path": {
165 | "tag": "Field",
166 | "model": "User",
167 | "field": "count"
168 | },
169 | "directive": "default"
170 | }
171 | },
172 | {
173 | "tag": "CreateArgument",
174 | "location": {
175 | "tag": "Directive",
176 | "path": {
177 | "tag": "Field",
178 | "model": "User",
179 | "field": "count"
180 | },
181 | "directive": "default"
182 | },
183 | "argument": "",
184 | "value": "0"
185 | },
186 | {
187 | "tag": "CreateField",
188 | "model": "User",
189 | "field": "books",
190 | "type": "Book",
191 | "arity": "List"
192 | },
193 | {
194 | "tag": "CreateField",
195 | "model": "User",
196 | "field": "profile",
197 | "type": "Profile",
198 | "arity": "Required"
199 | },
200 | {
201 | "tag": "CreateModel",
202 | "model": "Profile"
203 | },
204 | {
205 | "tag": "CreateField",
206 | "model": "Profile",
207 | "field": "id",
208 | "type": "Int",
209 | "arity": "Required"
210 | },
211 | {
212 | "tag": "CreateDirective",
213 | "location": {
214 | "path": {
215 | "tag": "Field",
216 | "model": "Profile",
217 | "field": "id"
218 | },
219 | "directive": "id"
220 | }
221 | },
222 | {
223 | "tag": "CreateDirective",
224 | "location": {
225 | "path": {
226 | "tag": "Field",
227 | "model": "Profile",
228 | "field": "id"
229 | },
230 | "directive": "default"
231 | }
232 | },
233 | {
234 | "tag": "CreateArgument",
235 | "location": {
236 | "tag": "Directive",
237 | "path": {
238 | "tag": "Field",
239 | "model": "Profile",
240 | "field": "id"
241 | },
242 | "directive": "default"
243 | },
244 | "argument": "",
245 | "value": "autoincrement()"
246 | },
247 | {
248 | "tag": "CreateField",
249 | "model": "Profile",
250 | "field": "bio",
251 | "type": "String",
252 | "arity": "Required"
253 | },
254 | {
255 | "tag": "CreateField",
256 | "model": "Profile",
257 | "field": "user",
258 | "type": "User",
259 | "arity": "Required"
260 | },
261 | {
262 | "tag": "CreateDirective",
263 | "location": {
264 | "path": {
265 | "tag": "Field",
266 | "model": "Profile",
267 | "field": "user"
268 | },
269 | "directive": "relation"
270 | }
271 | },
272 | {
273 | "tag": "CreateArgument",
274 | "location": {
275 | "tag": "Directive",
276 | "path": {
277 | "tag": "Field",
278 | "model": "Profile",
279 | "field": "user"
280 | },
281 | "directive": "relation"
282 | },
283 | "argument": "fields",
284 | "value": "[userId]"
285 | },
286 | {
287 | "tag": "CreateArgument",
288 | "location": {
289 | "tag": "Directive",
290 | "path": {
291 | "tag": "Field",
292 | "model": "Profile",
293 | "field": "user"
294 | },
295 | "directive": "relation"
296 | },
297 | "argument": "references",
298 | "value": "[id]"
299 | },
300 | {
301 | "tag": "CreateField",
302 | "model": "Profile",
303 | "field": "userId",
304 | "type": "Int",
305 | "arity": "Required"
306 | },
307 | {
308 | "tag": "CreateModel",
309 | "model": "Book"
310 | },
311 | {
312 | "tag": "CreateField",
313 | "model": "Book",
314 | "field": "id",
315 | "type": "Int",
316 | "arity": "Required"
317 | },
318 | {
319 | "tag": "CreateDirective",
320 | "location": {
321 | "path": {
322 | "tag": "Field",
323 | "model": "Book",
324 | "field": "id"
325 | },
326 | "directive": "id"
327 | }
328 | },
329 | {
330 | "tag": "CreateDirective",
331 | "location": {
332 | "path": {
333 | "tag": "Field",
334 | "model": "Book",
335 | "field": "id"
336 | },
337 | "directive": "default"
338 | }
339 | },
340 | {
341 | "tag": "CreateArgument",
342 | "location": {
343 | "tag": "Directive",
344 | "path": {
345 | "tag": "Field",
346 | "model": "Book",
347 | "field": "id"
348 | },
349 | "directive": "default"
350 | },
351 | "argument": "",
352 | "value": "autoincrement()"
353 | },
354 | {
355 | "tag": "CreateField",
356 | "model": "Book",
357 | "field": "title",
358 | "type": "String",
359 | "arity": "Required"
360 | },
361 | {
362 | "tag": "CreateField",
363 | "model": "Book",
364 | "field": "author",
365 | "type": "User",
366 | "arity": "Required"
367 | },
368 | {
369 | "tag": "CreateDirective",
370 | "location": {
371 | "path": {
372 | "tag": "Field",
373 | "model": "Book",
374 | "field": "author"
375 | },
376 | "directive": "relation"
377 | }
378 | },
379 | {
380 | "tag": "CreateArgument",
381 | "location": {
382 | "tag": "Directive",
383 | "path": {
384 | "tag": "Field",
385 | "model": "Book",
386 | "field": "author"
387 | },
388 | "directive": "relation"
389 | },
390 | "argument": "fields",
391 | "value": "[userId]"
392 | },
393 | {
394 | "tag": "CreateArgument",
395 | "location": {
396 | "tag": "Directive",
397 | "path": {
398 | "tag": "Field",
399 | "model": "Book",
400 | "field": "author"
401 | },
402 | "directive": "relation"
403 | },
404 | "argument": "references",
405 | "value": "[id]"
406 | },
407 | {
408 | "tag": "CreateField",
409 | "model": "Book",
410 | "field": "userId",
411 | "type": "Int",
412 | "arity": "Required"
413 | },
414 | {
415 | "tag": "CreateField",
416 | "model": "Book",
417 | "field": "categories",
418 | "type": "Category",
419 | "arity": "List"
420 | },
421 | {
422 | "tag": "CreateModel",
423 | "model": "Category"
424 | },
425 | {
426 | "tag": "CreateField",
427 | "model": "Category",
428 | "field": "id",
429 | "type": "Int",
430 | "arity": "Required"
431 | },
432 | {
433 | "tag": "CreateDirective",
434 | "location": {
435 | "path": {
436 | "tag": "Field",
437 | "model": "Category",
438 | "field": "id"
439 | },
440 | "directive": "id"
441 | }
442 | },
443 | {
444 | "tag": "CreateDirective",
445 | "location": {
446 | "path": {
447 | "tag": "Field",
448 | "model": "Category",
449 | "field": "id"
450 | },
451 | "directive": "default"
452 | }
453 | },
454 | {
455 | "tag": "CreateArgument",
456 | "location": {
457 | "tag": "Directive",
458 | "path": {
459 | "tag": "Field",
460 | "model": "Category",
461 | "field": "id"
462 | },
463 | "directive": "default"
464 | },
465 | "argument": "",
466 | "value": "autoincrement()"
467 | },
468 | {
469 | "tag": "CreateField",
470 | "model": "Category",
471 | "field": "books",
472 | "type": "Book",
473 | "arity": "List"
474 | }
475 | ]
476 | }
--------------------------------------------------------------------------------
/prisma/migrations/migrate.lock:
--------------------------------------------------------------------------------
1 | # Prisma Migrate lockfile v1
2 |
3 | 20201110101443
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | // This is your Prisma schema file,
2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema
3 |
4 | datasource db {
5 | provider = "postgresql"
6 | url = env("DATABASE_URL")
7 | }
8 |
9 | generator client {
10 | provider = "prisma-client-js"
11 | }
12 |
13 | enum Role {
14 | USER
15 | PLAYER
16 | SHOP
17 | ADMIN
18 | }
19 |
20 | model User {
21 | id Int @id @default(autoincrement())
22 | email String @unique
23 | firstName String
24 | lastName String
25 | password String
26 | role Role @default(USER)
27 | count Int @default(0)
28 | books Book[]
29 | profile Profile
30 | }
31 |
32 | model Profile {
33 | id Int @id @default(autoincrement())
34 | bio String
35 | user User @relation(fields: [userId], references: [id])
36 | userId Int
37 | }
38 |
39 | model Book {
40 | id Int @id @default(autoincrement())
41 | title String
42 | author User @relation(fields: [userId], references: [id])
43 | userId Int
44 | categories Category[]
45 | }
46 |
47 | model Category {
48 | id Int @id @default(autoincrement())
49 | books Book[]
50 | }
51 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/redbaron76/nextjs-api-routes-prisma-yup-chakra-ui/ee558f34616cb964b8946962d820ed1561a3734e/public/favicon.ico
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/Navbar.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import { useMutation, queryCache } from "react-query";
3 | import { Box, Flex, Heading, Text, Link } from "@chakra-ui/core";
4 | import { AuthContext } from "src/components/layouts/AuthLayout";
5 | import { IAuthContext, IMeUser } from "src/interfaces/user";
6 | import { HTTP } from "src/utils/http";
7 |
8 | interface LogoutResponse {
9 | success: boolean;
10 | }
11 |
12 | const Navbar = (props: any) => {
13 | const { userLogged, userData, userId } = useContext(AuthContext);
14 |
15 | const logoutUser = async () => {
16 | const response = await HTTP("/api/auth/logout", "POST", {
17 | userId,
18 | });
19 | return await response.json();
20 | };
21 |
22 | const [logout] = useMutation(logoutUser, {
23 | onSuccess: ({ success }) => {
24 | // reset AuthContext on logout
25 | if (success) return queryCache.setQueryData("me", null);
26 | },
27 | });
28 |
29 | return (
30 |
40 |
41 |
42 | RT Demo App
43 |
44 |
45 |
46 | {userData?.email || "Please, signup!"}
47 | {userLogged && (
48 |
49 | (logout)
50 |
51 | )}
52 |
53 |
54 | );
55 | };
56 |
57 | export default Navbar;
58 |
--------------------------------------------------------------------------------
/src/components/forms/CreateUserForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { mutateNewData } from "src/utils/cache";
3 | import { useForm } from "react-hook-form";
4 | import { HTTP } from "src/utils/http";
5 | import { setFormErrors } from "src/utils/validation";
6 | import { useMutation, queryCache } from "react-query";
7 | import {
8 | IUserCreate,
9 | IUserCreateErrors,
10 | IUserCreateResponse,
11 | } from "src/interfaces/user";
12 | import {
13 | Box,
14 | Button,
15 | FormControl,
16 | FormErrorMessage,
17 | FormLabel,
18 | Heading,
19 | Input,
20 | } from "@chakra-ui/core";
21 |
22 | const CreateUserForm: React.FC = () => {
23 | const createUser = async (newUser: IUserCreate) => {
24 | const response = await HTTP("/api/user/create", "POST", {
25 | create: newUser,
26 | });
27 | return await response.json();
28 | };
29 |
30 | const [mutate, { isLoading }] = useMutation<
31 | IUserCreateResponse,
32 | IUserCreateErrors,
33 | IUserCreate,
34 | () => void
35 | >(createUser, {
36 | onMutate: (newUser) => mutateNewData("users", newUser),
37 | onError: (_errors, _newData, rollback) => rollback(),
38 | onSettled: () => queryCache.refetchQueries("users"),
39 | });
40 |
41 | const { handleSubmit, register, reset, errors, setError } = useForm<
42 | IUserCreate
43 | >();
44 |
45 | const onSubmit = async (input: IUserCreate) => {
46 | const data = await mutate(input);
47 | const { user, errors }: IUserCreateResponse = data;
48 |
49 | if (errors) return setFormErrors(errors, setError);
50 |
51 | reset();
52 | return user;
53 | };
54 |
55 | return (
56 | <>
57 |
58 | Register
59 |
60 |
61 |
124 |
125 | >
126 | );
127 | };
128 |
129 | export default CreateUserForm;
130 |
--------------------------------------------------------------------------------
/src/components/forms/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useForm } from "react-hook-form";
3 | import { useMutation, queryCache } from "react-query";
4 | import { HTTP } from "src/utils/http";
5 | import {
6 | IUserLogin,
7 | IUserLoginError,
8 | IUserLoginResponse,
9 | } from "src/interfaces/user";
10 | import {
11 | Box,
12 | Button,
13 | FormControl,
14 | FormErrorMessage,
15 | FormLabel,
16 | Heading,
17 | Input,
18 | } from "@chakra-ui/core";
19 |
20 | const LoginForm: React.FC = () => {
21 | const loginUser = async (loginUser: IUserLogin) => {
22 | const response = await HTTP("/api/auth/login", "POST", {
23 | login: loginUser,
24 | });
25 | return await response.json();
26 | };
27 |
28 | const [mutate, { isLoading }] = useMutation<
29 | IUserLoginResponse,
30 | IUserLoginError,
31 | IUserLogin
32 | >(loginUser, {
33 | onSuccess: ({ user, error }) => {
34 | // update cache for "me" query
35 | if (user) return queryCache.setQueryData("me", user);
36 | // trigger error if not good login
37 | if (error) alert(error.message);
38 | },
39 | });
40 |
41 | const { handleSubmit, register, reset, errors, setError } = useForm<
42 | IUserLogin
43 | >();
44 |
45 | const onSubmit = async (login: IUserLogin) => {
46 | await mutate(login);
47 | };
48 |
49 | return (
50 | <>
51 |
52 | Login
53 |
54 |
55 |
84 |
85 | >
86 | );
87 | };
88 |
89 | export default LoginForm;
90 |
--------------------------------------------------------------------------------
/src/components/layouts/AuthLayout.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useQuery } from "react-query";
3 | import { Center, ChakraProvider, Spinner } from "@chakra-ui/core";
4 | import { IAuthContext, IMeUser } from "src/interfaces/user";
5 | import Navbar from "../Navbar";
6 | import { User } from "@prisma/client";
7 | import { HTTP } from "src/utils/http";
8 |
9 | const initialContext: IAuthContext = {
10 | userLogged: false,
11 | };
12 |
13 | export const AuthContext: React.Context = React.createContext(
14 | initialContext
15 | );
16 |
17 | async function getLoggedUser(): Promise {
18 | const response = await HTTP("api/auth/me", "POST");
19 | const { user }: { user: User | null } = await response.json();
20 | return user;
21 | }
22 |
23 | const AuthLayout: React.FC = ({ children }) => {
24 | const { data, isLoading } = useQuery("me", getLoggedUser);
25 | return (
26 |
27 | {isLoading ? (
28 |
29 |
30 |
31 | ) : (
32 |
39 |
40 | {children}
41 |
42 | )}
43 |
44 | );
45 | };
46 |
47 | export default AuthLayout;
48 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export const AUTH_TOKEN_NAME = "authToken";
2 | export const AUTH_TOKEN_EXPIRE = 60 * 15; // 15m
3 |
4 | export const REFRESH_TOKEN_NAME = "refreshToken";
5 | export const REFRESH_TOKEN_EXPIRE = 60 * 60 * 24 * 7; // 7d
6 |
7 | export const SUBSCR_NEW_USER = "SUBSCR_NEW_USER";
8 |
--------------------------------------------------------------------------------
/src/interfaces/user.ts:
--------------------------------------------------------------------------------
1 | import { Profile, Role } from "@prisma/client";
2 |
3 | // USER
4 |
5 | export interface IMeUser {
6 | id: number;
7 | firstName: string;
8 | lastName: string;
9 | email: string;
10 | role: Role;
11 | profile?: Profile;
12 | }
13 |
14 | export interface IAuthContext {
15 | userId?: number | null;
16 | userData?: IMeUser | null;
17 | userLogged: boolean;
18 | }
19 |
20 | // LOGIN
21 |
22 | export interface IUserLogin {
23 | email: string;
24 | password: string;
25 | }
26 |
27 | export interface IUserLoginError {
28 | message: string;
29 | }
30 |
31 | export interface IUserLoginResponse {
32 | user?: IMeUser;
33 | error?: IUserLoginError;
34 | }
35 |
36 | // CREATE
37 |
38 | export interface IUserCreate {
39 | firstName: string;
40 | lastName: string;
41 | email: string;
42 | password: string;
43 | confirmPassword: string;
44 | }
45 |
46 | export interface IUserCreateErrors {
47 | firstName?: string;
48 | lastName?: string;
49 | email?: string;
50 | password?: string;
51 | confirmPassword?: string;
52 | }
53 |
54 | export interface IUserCreateResponse {
55 | user?: IUserCreate;
56 | value?: IUserCreate;
57 | errors?: IUserCreateErrors;
58 | }
59 |
--------------------------------------------------------------------------------
/src/pages/_app.tsx:
--------------------------------------------------------------------------------
1 | import { NextPage } from "next";
2 | import { AppProps } from "next/dist/next-server/lib/router/router";
3 |
4 | import AuthLayout from "src/components/layouts/AuthLayout";
5 |
6 | const App: NextPage = ({ Component, pageProps }) => {
7 | return (
8 |
9 |
10 |
11 | );
12 | };
13 |
14 | export default App;
15 |
--------------------------------------------------------------------------------
/src/pages/api/auth/login.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { PrismaClient } from "@prisma/client";
3 | import { setCookies } from "src/utils/cookie";
4 | import bcrypt from "bcryptjs";
5 |
6 | import { IUserLogin } from "src/interfaces/user";
7 | import {
8 | AUTH_TOKEN_NAME,
9 | REFRESH_TOKEN_NAME,
10 | AUTH_TOKEN_EXPIRE,
11 | REFRESH_TOKEN_EXPIRE,
12 | } from "src/constants";
13 | import { reduceMeUser } from "src/utils/format";
14 |
15 | export default async (req: NextApiRequest, res: NextApiResponse) => {
16 | if (req.method === "POST") {
17 | const prisma = new PrismaClient({ log: ["query", "info"] });
18 | try {
19 | const { login: data }: { login: IUserLogin } = req.body;
20 | const user = await prisma.user.findOne({
21 | where: {
22 | email: data.email,
23 | },
24 | include: {
25 | profile: true,
26 | },
27 | });
28 |
29 | if (!user) throw new Error("User not valid");
30 |
31 | const validPassword = await bcrypt.compare(data.password, user.password);
32 | if (!validPassword) throw new Error("Password not valid");
33 |
34 | await setCookies(res, [
35 | {
36 | tokenName: AUTH_TOKEN_NAME,
37 | payload: { userId: user.id },
38 | expire: AUTH_TOKEN_EXPIRE,
39 | secret: process.env.JWT_AUTH_SECRET,
40 | },
41 | {
42 | tokenName: REFRESH_TOKEN_NAME,
43 | payload: { userId: user.id, count: user.count },
44 | expire: REFRESH_TOKEN_EXPIRE,
45 | secret: process.env.JWT_REFRESH_SECRET,
46 | },
47 | ]);
48 |
49 | // return logged-in user
50 | res.status(200).json({ user: reduceMeUser(user) });
51 | } catch (error) {
52 | // return error message
53 | res.status(200).json({ error: { message: error.message } });
54 | } finally {
55 | // disconnect at the end
56 | await prisma.$disconnect();
57 | }
58 | }
59 | res.status(405).end();
60 | };
61 |
--------------------------------------------------------------------------------
/src/pages/api/auth/logout.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { PrismaClient } from "@prisma/client";
3 | import { setCookies } from "src/utils/cookie";
4 |
5 | import { AUTH_TOKEN_NAME } from "src/constants";
6 |
7 | export default async (req: NextApiRequest, res: NextApiResponse) => {
8 | if (req.method === "POST") {
9 | const prisma = new PrismaClient({ log: ["query", "info"] });
10 | try {
11 | const { userId }: { userId: number } = req.body;
12 | const user = await prisma.user.update({
13 | where: { id: userId },
14 | data: {
15 | count: {
16 | increment: 1,
17 | },
18 | },
19 | });
20 |
21 | if (!user) throw new Error("User not found");
22 |
23 | await setCookies(res, [
24 | {
25 | tokenName: AUTH_TOKEN_NAME,
26 | expire: -1,
27 | },
28 | ]);
29 |
30 | // return success as true
31 | res.status(200).json({ success: true });
32 | } catch (error) {
33 | // return error as false
34 | res.status(200).json({ success: false });
35 | } finally {
36 | // disconnect at the end
37 | await prisma.$disconnect();
38 | }
39 | }
40 | res.status(405).end();
41 | };
42 |
--------------------------------------------------------------------------------
/src/pages/api/auth/me.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { PrismaClient } from "@prisma/client";
3 | import { setCookies } from "src/utils/cookie";
4 | import { verify } from "jsonwebtoken";
5 | import { isNumber } from "lodash";
6 | import cookie from "cookie";
7 |
8 | import {
9 | REFRESH_TOKEN_EXPIRE,
10 | AUTH_TOKEN_EXPIRE,
11 | AUTH_TOKEN_NAME,
12 | REFRESH_TOKEN_NAME,
13 | } from "src/constants";
14 |
15 | interface Token {
16 | userId?: number;
17 | }
18 |
19 | interface RefreshToken extends Token {
20 | count?: number;
21 | }
22 |
23 | const getUserId = async (req: NextApiRequest): Promise => {
24 | // get parsed cookies from headers
25 | const parsedCookies = req && cookie.parse(req.headers.cookie ?? "");
26 |
27 | if (parsedCookies) {
28 | const authToken = parsedCookies[AUTH_TOKEN_NAME];
29 | const refreshToken = parsedCookies[REFRESH_TOKEN_NAME];
30 |
31 | // exit with undefined if no tokens
32 | if (!refreshToken && !authToken)
33 | return {
34 | userId: null,
35 | };
36 |
37 | try {
38 | const verifiedToken = verify(
39 | authToken,
40 | process.env.JWT_AUTH_SECRET
41 | ) as Token;
42 |
43 | // GOOD authToken -> set userId in context
44 | return {
45 | userId: verifiedToken && verifiedToken.userId,
46 | };
47 | } catch {
48 | // authToken expired -> check refreshToken
49 | }
50 |
51 | // BAD refreshToken and no authToken -> exit with undefined
52 | if (!refreshToken) return;
53 |
54 | let verifiedToken: RefreshToken;
55 |
56 | try {
57 | verifiedToken = verify(
58 | refreshToken,
59 | process.env.JWT_REFRESH_SECRET
60 | ) as RefreshToken;
61 | } catch {
62 | return {
63 | userId: null,
64 | };
65 | }
66 |
67 | // get saved items from refreshToken
68 | return verifiedToken;
69 | }
70 |
71 | return {
72 | userId: null,
73 | };
74 | };
75 |
76 | export default async (req: NextApiRequest, res: NextApiResponse) => {
77 | if (req.method === "POST") {
78 | const prisma = new PrismaClient({ log: ["query", "info"] });
79 | const { userId, count } = await getUserId(req);
80 |
81 | if (!userId) {
82 | res.status(200).json({ user: null });
83 | return;
84 | }
85 |
86 | let user = await prisma.user.findOne({
87 | where: {
88 | id: userId,
89 | },
90 | });
91 |
92 | if (!user || (isNumber(count) && user.count !== count)) {
93 | res.status(200).json({ user: null });
94 | return;
95 | }
96 |
97 | await setCookies(res, [
98 | {
99 | tokenName: AUTH_TOKEN_NAME,
100 | payload: { userId: user.id },
101 | expire: AUTH_TOKEN_EXPIRE,
102 | secret: process.env.JWT_AUTH_SECRET,
103 | },
104 | {
105 | tokenName: REFRESH_TOKEN_NAME,
106 | payload: { userId: user.id, count: user.count },
107 | expire: REFRESH_TOKEN_EXPIRE,
108 | secret: process.env.JWT_REFRESH_SECRET,
109 | },
110 | ]);
111 |
112 | res.status(200).json({ user });
113 | }
114 | res.status(405).end();
115 | };
116 |
--------------------------------------------------------------------------------
/src/pages/api/socketio.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest } from "next";
2 | import { NextApiResponseServerIO } from "src/types/next";
3 | import { Server as ServerIO } from "socket.io";
4 | import { Server as NetServer } from "http";
5 |
6 | export const config = {
7 | api: {
8 | bodyParser: false,
9 | },
10 | };
11 |
12 | export default async (req: NextApiRequest, res: NextApiResponseServerIO) => {
13 | if (!res.socket.server.io) {
14 | console.log("New Socket.io server...");
15 | // adapt Next's net Server to http Server
16 | const httpServer: NetServer = res.socket.server as any;
17 | const io = new ServerIO(httpServer, {
18 | path: "/api/socketio",
19 | });
20 | // append SocketIO server to Next.js socket server response
21 | res.socket.server.io = io;
22 | }
23 | res.end();
24 | };
25 |
--------------------------------------------------------------------------------
/src/pages/api/user/create.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { NextApiResponseServerIO } from "src/types/next";
3 | import { PrismaClient } from "@prisma/client";
4 | import bcrypt from "bcryptjs";
5 |
6 | import { IUserCreate } from "src/interfaces/user";
7 | import { validUserCreate } from "src/utils/validators/user";
8 | import { generateErrors } from "src/utils/validation";
9 |
10 | export default async (req: NextApiRequest, res: NextApiResponseServerIO) => {
11 | if (req.method === "POST") {
12 | const prisma = new PrismaClient({ log: ["query", "info"] });
13 | const { create: data }: { create: IUserCreate } = req.body;
14 | await validUserCreate
15 | .validate(data, {
16 | abortEarly: false,
17 | })
18 | .then(async (valid) => {
19 | if (valid) {
20 | delete data.confirmPassword;
21 | try {
22 | data.password = await bcrypt.hash(data.password, 12);
23 | const newUser = await prisma.user.create({
24 | data: {
25 | ...data,
26 | profile: {
27 | create: {
28 | bio: "",
29 | },
30 | },
31 | },
32 | });
33 |
34 | // Trigger event in useQuerySocket
35 | res.socket.server.io.emit("users", newUser);
36 |
37 | res.status(201).json({ data });
38 | } catch (err) {
39 | console.log("ERR", err);
40 | res.status(400).end();
41 | }
42 | }
43 | })
44 | .catch(({ value, inner }) => {
45 | res.status(200).json({ value, errors: generateErrors(inner) });
46 | })
47 | .finally(async () => {
48 | await prisma.$disconnect();
49 | });
50 | }
51 | res.status(405).end();
52 | };
53 |
--------------------------------------------------------------------------------
/src/pages/api/user/get.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from "next";
2 | import { PrismaClient } from "@prisma/client";
3 |
4 | export default async (req: NextApiRequest, res: NextApiResponse) => {
5 | if (req.method === "GET") {
6 | const prisma = new PrismaClient({ log: ["query", "info"] });
7 | const users = await prisma.user.findMany({
8 | orderBy: {
9 | id: "asc",
10 | },
11 | });
12 |
13 | res.status(200).json({ users });
14 | }
15 | res.status(405).end();
16 | };
17 |
--------------------------------------------------------------------------------
/src/pages/index.tsx:
--------------------------------------------------------------------------------
1 | import { useContext } from "react";
2 | import Head from "next/head";
3 | import { AuthContext } from "src/components/layouts/AuthLayout";
4 | import { Box, Flex, UnorderedList, ListItem, Heading } from "@chakra-ui/core";
5 | import CreateUserForm from "src/components/forms/CreateUserForm";
6 | import useQuerySocket from "src/utils/hooks/useQuerySocket";
7 | import LoginForm from "src/components/forms/LoginForm";
8 | import { User } from "@prisma/client";
9 | import { HTTP } from "src/utils/http";
10 |
11 | async function fetchUsers(): Promise {
12 | const response = await HTTP("/api/user/get", "GET");
13 | const { users }: { users: User[] | [] } = await response.json();
14 | return users;
15 | }
16 |
17 | export default function Home() {
18 | const { userLogged } = useContext(AuthContext);
19 |
20 | // Fetch users and keep track of changes in "users" in real-time
21 | const { data: users, isLoading } = useQuerySocket(
22 | "users",
23 | fetchUsers
24 | );
25 |
26 | if (!users || isLoading) return Loading...;
27 |
28 | return (
29 |
30 |
31 | App Demo ChakraUI
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Users
42 |
43 |
44 | {users.map((user) => (
45 | {`${user.firstName} ${user.lastName} (${user.email})`}
48 | ))}
49 |
50 |
51 | {!userLogged && (
52 |
53 |
54 |
55 | )}
56 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/src/styles/Home.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | min-height: 100vh;
3 | padding: 0 0.5rem;
4 | display: flex;
5 | flex-direction: column;
6 | justify-content: center;
7 | align-items: center;
8 | }
9 |
10 | .main {
11 | padding: 5rem 0;
12 | flex: 1;
13 | display: flex;
14 | flex-direction: column;
15 | justify-content: center;
16 | align-items: center;
17 | }
18 |
19 | .footer {
20 | width: 100%;
21 | height: 100px;
22 | border-top: 1px solid #eaeaea;
23 | display: flex;
24 | justify-content: center;
25 | align-items: center;
26 | }
27 |
28 | .footer img {
29 | margin-left: 0.5rem;
30 | }
31 |
32 | .footer a {
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | }
37 |
38 | .title a {
39 | color: #0070f3;
40 | text-decoration: none;
41 | }
42 |
43 | .title a:hover,
44 | .title a:focus,
45 | .title a:active {
46 | text-decoration: underline;
47 | }
48 |
49 | .title {
50 | margin: 0;
51 | line-height: 1.15;
52 | font-size: 4rem;
53 | }
54 |
55 | .title,
56 | .description {
57 | text-align: center;
58 | }
59 |
60 | .description {
61 | line-height: 1.5;
62 | font-size: 1.5rem;
63 | }
64 |
65 | .code {
66 | background: #fafafa;
67 | border-radius: 5px;
68 | padding: 0.75rem;
69 | font-size: 1.1rem;
70 | font-family: Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono,
71 | Bitstream Vera Sans Mono, Courier New, monospace;
72 | }
73 |
74 | .grid {
75 | display: flex;
76 | align-items: center;
77 | justify-content: center;
78 | flex-wrap: wrap;
79 | max-width: 800px;
80 | margin-top: 3rem;
81 | }
82 |
83 | .card {
84 | margin: 1rem;
85 | flex-basis: 45%;
86 | padding: 1.5rem;
87 | text-align: left;
88 | color: inherit;
89 | text-decoration: none;
90 | border: 1px solid #eaeaea;
91 | border-radius: 10px;
92 | transition: color 0.15s ease, border-color 0.15s ease;
93 | }
94 |
95 | .card:hover,
96 | .card:focus,
97 | .card:active {
98 | color: #0070f3;
99 | border-color: #0070f3;
100 | }
101 |
102 | .card h3 {
103 | margin: 0 0 1rem 0;
104 | font-size: 1.5rem;
105 | }
106 |
107 | .card p {
108 | margin: 0;
109 | font-size: 1.25rem;
110 | line-height: 1.5;
111 | }
112 |
113 | .logo {
114 | height: 1em;
115 | }
116 |
117 | @media (max-width: 600px) {
118 | .grid {
119 | width: 100%;
120 | flex-direction: column;
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/styles/globals.css:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | padding: 0;
4 | margin: 0;
5 | font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
6 | Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
7 | }
8 |
9 | a {
10 | color: inherit;
11 | text-decoration: none;
12 | }
13 |
14 | * {
15 | box-sizing: border-box;
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/next.ts:
--------------------------------------------------------------------------------
1 | import { Server as NetServer, Socket } from "net";
2 | import { NextApiResponse } from "next";
3 | import { Server as SocketIOServer } from "socket.io";
4 |
5 | export type NextApiResponseServerIO = NextApiResponse & {
6 | socket: Socket & {
7 | server: NetServer & {
8 | io: SocketIOServer;
9 | };
10 | };
11 | };
12 |
--------------------------------------------------------------------------------
/src/types/user.ts:
--------------------------------------------------------------------------------
1 | import { Profile, User } from "@prisma/client";
2 |
3 | export type UserWithProfile = User & {
4 | profile: Profile;
5 | };
6 |
--------------------------------------------------------------------------------
/src/utils/cache.ts:
--------------------------------------------------------------------------------
1 | import { queryCache } from "react-query";
2 |
3 | /**
4 | * Perform onMutate callback on mutate data
5 | * @param cacheKey string or array
6 | * @param newData object
7 | */
8 | export const mutateNewData = (cacheKey: any | [], newData: object) => {
9 | queryCache.cancelQueries(cacheKey);
10 | const prevCacheData = queryCache.getQueryData(cacheKey);
11 | queryCache.setQueryData(cacheKey, (prev: I[]) => [
12 | ...prev,
13 | { ...newData, id: fakeId() },
14 | ]);
15 | return () => queryCache.setQueryData(cacheKey, prevCacheData);
16 | };
17 |
18 | export const fakeId = (): number => {
19 | return new Date().getTime();
20 | };
21 |
--------------------------------------------------------------------------------
/src/utils/cookie.ts:
--------------------------------------------------------------------------------
1 | import cookie from "cookie";
2 | import { sign } from "jsonwebtoken";
3 |
4 | import { NextApiResponse } from "next";
5 |
6 | export async function setCookie(
7 | res: NextApiResponse,
8 | tokenName: string = "token",
9 | payload: object | string,
10 | expire: number = 60 * 60 * 6, // 6h
11 | secret: string = process.env.JWT_AUTH_SECRET
12 | ): Promise {
13 | res.setHeader(
14 | "Set-Cookie",
15 | cookie.serialize(tokenName, payload ? sign(payload, secret) : "", {
16 | httpOnly: true,
17 | maxAge: expire,
18 | path: "/",
19 | sameSite: "lax",
20 | secure: process.env.NODE_ENV === "production",
21 | })
22 | );
23 | }
24 |
25 | interface Cookie {
26 | tokenName: string;
27 | payload?: object;
28 | expire: number;
29 | secret?: string;
30 | }
31 |
32 | export async function setCookies(
33 | res: NextApiResponse,
34 | cookies: Cookie[]
35 | ): Promise {
36 | res.setHeader(
37 | "Set-Cookie",
38 | cookies.map((c) => {
39 | return cookie.serialize(
40 | c.tokenName,
41 | c.payload ? sign(c.payload, c.secret) : "",
42 | {
43 | httpOnly: true,
44 | maxAge: c.expire,
45 | path: "/",
46 | sameSite: "lax",
47 | secure: process.env.NODE_ENV === "production",
48 | }
49 | );
50 | })
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/utils/format.ts:
--------------------------------------------------------------------------------
1 | import { omit } from "lodash";
2 | import { UserWithProfile } from "src/types/user";
3 | import { IMeUser } from "src/interfaces/user";
4 |
5 | export const reduceMeUser = (user: UserWithProfile): IMeUser => {
6 | // Remove password and count from logged in user return
7 | return omit(user, ["password", "count"]);
8 | };
9 |
--------------------------------------------------------------------------------
/src/utils/hooks/useQuerySocket.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 | import { useQuery } from "react-query";
3 | import { io } from "socket.io-client";
4 |
5 | export default function useQuerySocket(
6 | cacheKey: any,
7 | callback: () => Promise
8 | ) {
9 | const { data, isLoading, refetch } = useQuery(cacheKey, callback);
10 |
11 | useEffect(() => {
12 | const socket = io({
13 | path: "/api/socketio",
14 | });
15 |
16 | socket.on(cacheKey, (_emitParam: any) => {
17 | // refetch query on server emit
18 | refetch();
19 | });
20 |
21 | // socket disconnet onUnmount
22 | return () => socket.disconnect();
23 | }, []);
24 |
25 | return { data, isLoading };
26 | }
27 |
--------------------------------------------------------------------------------
/src/utils/http.ts:
--------------------------------------------------------------------------------
1 | import { isEmpty } from "lodash";
2 |
3 | /**
4 | * fetch() wrapper
5 | * @param uri /api
6 | * @param method POST
7 | * @param body empty obj {}
8 | * @param contentType application/json
9 | */
10 | export const HTTP = async (
11 | uri: string = "/api",
12 | method: string = "POST",
13 | body: object = {},
14 | contentType: string = "application/json"
15 | ): Promise => {
16 | return await fetch(uri, {
17 | method,
18 | headers: {
19 | "Content-Type": contentType,
20 | },
21 | body: !isEmpty(body) ? JSON.stringify(body) : null,
22 | });
23 | };
24 |
--------------------------------------------------------------------------------
/src/utils/validation.ts:
--------------------------------------------------------------------------------
1 | import { ValidationError } from "yup";
2 | import { IUserCreateErrors } from "src/interfaces/user";
3 | import { ErrorOption } from "react-hook-form";
4 |
5 | export const generateErrors = (inner: ValidationError[]): IUserCreateErrors => {
6 | if (inner && inner.length) {
7 | return inner.reduce((obj, e) => {
8 | obj[e.path] = e.errors[0];
9 | return obj;
10 | }, {});
11 | }
12 | };
13 |
14 | export const setFormErrors = (
15 | errors: IUserCreateErrors,
16 | setError: (prop: keyof IUserCreateErrors, error: ErrorOption) => void
17 | ): void => {
18 | Object.keys(errors).forEach((prop: keyof IUserCreateErrors) => {
19 | setError(prop, {
20 | type: "required",
21 | message: errors[prop],
22 | });
23 | });
24 | return;
25 | };
26 |
--------------------------------------------------------------------------------
/src/utils/validators/user.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from "@prisma/client";
2 | import { IUserCreate } from "src/interfaces/user";
3 | import * as yup from "yup";
4 |
5 | export const validUserCreate = yup.object().shape({
6 | firstName: yup.string().required().min(2),
7 | lastName: yup.string().required().min(2),
8 | password: yup.string().required().min(8),
9 | confirmPassword: yup.string().oneOf([yup.ref("password"), null]),
10 | email: yup
11 | .string()
12 | .required("This field is required")
13 | .email("This field must be an e-mail address")
14 | .test("unique-email", "This e-mail is already in use", async function (
15 | email: string
16 | ): Promise {
17 | const prisma = new PrismaClient({ log: ["query", "info"] });
18 | // const {options} = this;
19 | const user = await prisma.user.findOne({
20 | where: {
21 | email,
22 | },
23 | });
24 | await prisma.$disconnect();
25 | return !user;
26 | }),
27 | });
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "strict": false,
8 | "forceConsistentCasingInFileNames": true,
9 | "noEmit": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "rootDir": "src"
18 | },
19 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
20 | "exclude": ["node_modules"]
21 | }
22 |
--------------------------------------------------------------------------------