109 Commits

Author SHA1 Message Date
dependabot[bot]
9787fdab97 Bump http-proxy-middleware
Bumps the npm_and_yarn group with 1 update in the /client directory: [http-proxy-middleware](https://github.com/chimurai/http-proxy-middleware).


Updates `http-proxy-middleware` from 2.0.7 to 2.0.9
- [Release notes](https://github.com/chimurai/http-proxy-middleware/releases)
- [Changelog](https://github.com/chimurai/http-proxy-middleware/blob/v2.0.9/CHANGELOG.md)
- [Commits](https://github.com/chimurai/http-proxy-middleware/compare/v2.0.7...v2.0.9)

---
updated-dependencies:
- dependency-name: http-proxy-middleware
  dependency-version: 2.0.9
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-09 08:52:59 +00:00
dependabot[bot]
2edcd120e3 Bump mongoose from 8.10.1 to 8.15.1 in /server (#64)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Elferink <ben.elferink@icloud.com>
2025-06-09 11:31:14 +03:00
dependabot[bot]
cb29845c1a Bump react-dom from 19.0.0 to 19.1.0 in /client (#53)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Ben Elferink <ben.elferink@icloud.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Elferink <ben.elferink@icloud.com>
2025-06-09 11:00:28 +03:00
dependabot[bot]
a94bc4a0a7 Bump react and @types/react in /client (#55)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-09 10:59:31 +03:00
dependabot[bot]
f03cca1b35 Bump @types/node from 22.13.5 to 22.13.12 in /server (#50)
Signed-off-by: dependabot[bot] <support@github.com>
Signed-off-by: Ben Elferink <ben.elferink@icloud.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ben Elferink <ben.elferink@icloud.com>
2025-03-24 20:48:45 +02:00
dependabot[bot]
85c5812a00 Bump the npm_and_yarn group across 1 directory with 2 updates (#51)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 20:38:01 +02:00
dependabot[bot]
eb54852ed3 Bump axios from 1.7.9 to 1.8.4 in /client (#49)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 20:29:56 +02:00
dependabot[bot]
4569122e80 Bump @mui/material from 6.4.5 to 6.4.8 in /client (#48)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 20:26:30 +02:00
dependabot[bot]
db73e2c93d Bump @types/jsonwebtoken from 9.0.8 to 9.0.9 in /server (#42)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 20:24:20 +02:00
dependabot[bot]
324febb0b2 Bump typescript from 5.7.3 to 5.8.2 in /server (#41)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-24 20:24:08 +02:00
dependabot[bot]
6944110b79 Bump @types/react-dom from 19.0.3 to 19.0.4 in /client (#37)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 19:15:22 +02:00
dependabot[bot]
aa3aa6497b Bump @mui/material from 6.4.1 to 6.4.5 in /client (#35)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 19:14:01 +02:00
dependabot[bot]
bc0e7be940 Bump @types/react from 19.0.8 to 19.0.10 in /client (#36)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 19:13:40 +02:00
dependabot[bot]
2264c77055 Bump @types/node from 22.10.10 to 22.13.5 in /server (#38)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 19:13:26 +02:00
dependabot[bot]
299974e483 Bump mongoose from 8.9.5 to 8.10.1 in /server (#33)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-28 19:11:16 +02:00
Ben Elferink
36f565cf83 ci: add "build" workflows (#24) 2025-01-28 20:02:53 +02:00
Ben Elferink
a1ad99d821 Merge pull request #23 from BenElferink/BenElferink-patch-1
Update README.md
2025-01-27 19:31:28 +02:00
Ben Elferink
312b7a4201 Update README.md
Resolves #4
2025-01-27 19:31:06 +02:00
Ben Elferink
5b8325eb4d Merge pull request #21 from BenElferink/dependabot/npm_and_yarn/server/types/node-22.10.10
Bump @types/node from 22.10.7 to 22.10.10 in /server
2025-01-27 19:30:06 +02:00
dependabot[bot]
32a293fd7d Bump @types/node from 22.10.7 to 22.10.10 in /server
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.10.7 to 22.10.10.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 17:29:54 +00:00
Ben Elferink
283f2b7e03 Merge pull request #22 from BenElferink/dependabot/npm_and_yarn/server/types/jsonwebtoken-9.0.8
Bump @types/jsonwebtoken from 9.0.7 to 9.0.8 in /server
2025-01-27 19:29:20 +02:00
Ben Elferink
3af6df6204 Merge pull request #20 from BenElferink/dependabot/npm_and_yarn/server/typescript-5.7.3
Bump typescript from 5.7.2 to 5.7.3 in /server
2025-01-27 19:28:50 +02:00
Ben Elferink
909453634a Merge pull request #19 from BenElferink/dependabot/npm_and_yarn/client/zustand-5.0.3
Bump zustand from 5.0.2 to 5.0.3 in /client
2025-01-27 19:28:40 +02:00
Ben Elferink
774d78e526 Merge pull request #18 from BenElferink/dependabot/npm_and_yarn/client/mui/material-6.4.1
Bump @mui/material from 6.4.0 to 6.4.1 in /client
2025-01-27 19:28:24 +02:00
dependabot[bot]
8c52211dda Bump @types/jsonwebtoken from 9.0.7 to 9.0.8 in /server
Bumps [@types/jsonwebtoken](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jsonwebtoken) from 9.0.7 to 9.0.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jsonwebtoken)

---
updated-dependencies:
- dependency-name: "@types/jsonwebtoken"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 07:44:37 +00:00
dependabot[bot]
796c6b256a Bump typescript from 5.7.2 to 5.7.3 in /server
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.7.2 to 5.7.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Changelog](https://github.com/microsoft/TypeScript/blob/main/azure-pipelines.release.yml)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.7.2...v5.7.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 07:44:26 +00:00
dependabot[bot]
27a0b779f0 Bump zustand from 5.0.2 to 5.0.3 in /client
Bumps [zustand](https://github.com/pmndrs/zustand) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.2...v5.0.3)

---
updated-dependencies:
- dependency-name: zustand
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 07:44:19 +00:00
dependabot[bot]
2a668c4650 Bump @mui/material from 6.4.0 to 6.4.1 in /client
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.1/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 07:44:12 +00:00
Ben Elferink
ee1ffc33e2 Merge pull request #17 from BenElferink/dependabot/npm_and_yarn/client/types/react-19.0.8
Bump @types/react from 19.0.1 to 19.0.8 in /client
2025-01-27 09:12:28 +02:00
dependabot[bot]
1866dad550 Bump @types/react from 19.0.1 to 19.0.8 in /client
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 19.0.1 to 19.0.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-27 07:11:24 +00:00
Ben Elferink
fd45e04f44 Merge pull request #10 from BenElferink/dependabot/npm_and_yarn/client/types/react-dom-19.0.3
Bump @types/react-dom from 19.0.2 to 19.0.3 in /client
2025-01-27 09:10:21 +02:00
Ben Elferink
04cccbe825 Merge pull request #14 from BenElferink/dependabot/npm_and_yarn/client/mui/material-6.4.0
Bump @mui/material from 6.2.0 to 6.4.0 in /client
2025-01-27 09:10:09 +02:00
Ben Elferink
3bb7ccce85 Merge pull request #16 from BenElferink/dependabot/npm_and_yarn/server/types/node-22.10.7
Bump @types/node from 22.10.2 to 22.10.7 in /server
2025-01-26 20:55:37 +02:00
Ben Elferink
90f8d73a05 Merge pull request #13 from BenElferink/dependabot/npm_and_yarn/server/npm_and_yarn-907b4dc93e
Bump mongoose from 8.9.0 to 8.9.5 in /server in the npm_and_yarn group across 1 directory
2025-01-26 20:55:20 +02:00
dependabot[bot]
7bcd917fd9 Bump mongoose in /server in the npm_and_yarn group across 1 directory
Bumps the npm_and_yarn group with 1 update in the /server directory: [mongoose](https://github.com/Automattic/mongoose).


Updates `mongoose` from 8.9.0 to 8.9.5
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.9.0...8.9.5)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-26 18:54:58 +00:00
Ben Elferink
8c707db399 Merge pull request #12 from BenElferink/dependabot/npm_and_yarn/server/mongoose-8.9.4
Bump mongoose from 8.9.0 to 8.9.4 in /server
2025-01-26 20:53:50 +02:00
dependabot[bot]
8cf53c126b Bump @types/node from 22.10.2 to 22.10.7 in /server
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 22.10.2 to 22.10.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:07:35 +00:00
dependabot[bot]
723378424d Bump @mui/material from 6.2.0 to 6.4.0 in /client
Bumps [@mui/material](https://github.com/mui/material-ui/tree/HEAD/packages/mui-material) from 6.2.0 to 6.4.0.
- [Release notes](https://github.com/mui/material-ui/releases)
- [Changelog](https://github.com/mui/material-ui/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mui/material-ui/commits/v6.4.0/packages/mui-material)

---
updated-dependencies:
- dependency-name: "@mui/material"
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-20 08:07:12 +00:00
dependabot[bot]
fb4e459778 Bump mongoose from 8.9.0 to 8.9.4 in /server
Bumps [mongoose](https://github.com/Automattic/mongoose) from 8.9.0 to 8.9.4.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.9.0...8.9.4)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 07:53:46 +00:00
dependabot[bot]
af1204e7ca Bump @types/react-dom from 19.0.2 to 19.0.3 in /client
Bumps [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) from 19.0.2 to 19.0.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react-dom"
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-13 07:53:17 +00:00
Ben Elferink
f06e65b22d fix: update dependabot.yml 2025-01-06 21:52:20 +02:00
Ben Elferink
142356655d chore: create dependabot.yml 2025-01-06 21:50:34 +02:00
Ben Elferink
ce8bcead85 Merge pull request #5 from BenElferink/v2
feat: V2 with TypeScript
2024-12-14 16:18:53 +02:00
Ben Elferink
a41cab872f feat: V2 with TypeScript 2024-12-14 16:14:14 +02:00
Ben Elferink
f6c0a52ab5 Update README.md 2023-09-16 23:58:07 +03:00
Ben Elferink
b9655590f3 chore: updated dependencies 2021-10-27 19:31:20 +03:00
Ben Elferink
1a841acaa2 fix: friendly syntax 2021-10-27 19:31:01 +03:00
Ben Elferink
8540360217 tweak: utils and constants - frontend 2021-10-27 19:26:02 +03:00
Ben Elferink
75979bce3d fix: login with token returns new token 2021-10-27 19:06:47 +03:00
Ben Elferink
e9e8584d82 added 2 custom hooks 2021-10-02 14:24:36 +03:00
Ben Elferink
8c91a055f4 Added useful helper functions 2021-09-29 12:07:39 +03:00
Ben Elferink
c0cc342627 Updated readme 2021-09-29 11:09:19 +03:00
Ben Elferink
733d48c4de Updated readme 2021-09-29 11:04:01 +03:00
Ben Elferink
ebae7d478e Updated header 2021-09-29 11:03:56 +03:00
Ben Elferink
8de38c6536 Add role to account 2021-09-29 10:53:39 +03:00
Ben Elferink
5c3c54b4a9 usernames lowercased 2021-09-29 10:48:48 +03:00
Ben Elferink
b629a7014e improved auth methods 2021-09-29 10:45:40 +03:00
Ben Elferink
562e59d588 updated readme 2021-09-28 19:06:22 +03:00
Ben Elferink
f2333078ca bug fixes 2021-09-28 18:59:17 +03:00
Ben Elferink
2f40cf797c Refactored - now includes fullstack AUTH 2021-09-28 18:53:02 +03:00
Ben Elferink
c349907802 small edits 2021-03-13 17:49:21 +02:00
Ben Elferink
bc37d68394 updated dependecies, added context, axios config 2021-03-13 17:46:51 +02:00
Ben Elferink
a895cc0647 dependecies versions updated 2021-02-05 15:12:46 +02:00
Ben Elferink
60cf80ece7 more configs 2021-02-03 22:44:40 +02:00
Ben Elferink
1e86859cc1 added favicon HOW TO guide 2021-02-03 18:20:37 +02:00
Ben Elferink
d1d3040edb manifest 2021-02-03 18:20:23 +02:00
Ben Elferink
8ac43d1992 robots 2021-02-03 18:20:16 +02:00
Ben Elferink
2ba6b8bc59 meta tags 2021-02-03 18:20:10 +02:00
Ben Elferink
1e1039accd responsive breakpoints 2021-02-03 18:19:58 +02:00
Ben Elferink
5b26c86dfe updated some small details 2021-02-03 17:39:23 +02:00
Ben Elferink
711c0fc5ee refactored template 2021-01-19 22:33:49 +02:00
Ben Elferink
41d557dca6 Slightly updated template 2021-01-12 23:11:28 +02:00
Ben Elferink
2b1c80743a Update server.js 2020-12-31 03:48:12 +02:00
Ben Elferink
a205f496c9 Update model.js 2020-12-31 03:21:39 +02:00
Ben Elferink
c2b5e7bccd Update model.js 2020-12-30 19:09:52 +02:00
Ben Elferink
4e901d75ae Update README.md 2020-12-30 18:16:17 +02:00
Ben Elferink
be3e2892d2 typo 2020-12-30 17:20:20 +02:00
Ben Elferink
087fb3e7ef Update README.md 2020-12-30 16:28:22 +02:00
Ben Elferink
50943d3a7c Update README.md 2020-12-30 16:27:26 +02:00
Ben Elferink
6a4ac3b31d Update README.md 2020-12-30 16:27:03 +02:00
Ben Elferink
b2ae06f7cf Installation is even easier now! 2020-12-30 15:53:33 +02:00
Ben Elferink
ab6817973d removed nodemon from dependecies
Most devs have Nodemon installed globally on their system, so I removed the dependecy, and when a user types "npm start" on the server app, it will first look for nodemon, and if not found then it will do the standard "node server.js"
2020-12-30 15:32:33 +02:00
Ben Elferink
1c9a390ee7 deleted folder 2020-12-29 19:44:50 +02:00
Ben Elferink
4507903157 implemented reset stylesheet 2020-12-29 19:42:12 +02:00
Ben Elferink
f47b0c4eb0 moved css module example to component exmaple 2020-12-29 19:41:55 +02:00
Ben Elferink
833ac944c3 added timestamps to example Schema 2020-12-29 17:53:47 +02:00
Ben Elferink
f862566c54 severe typos 2020-12-27 02:06:00 +02:00
Ben Elferink
52dc9bd1e9 hotfix: forgot to refine controllers for dev-exp 2020-12-27 02:03:19 +02:00
Ben Elferink
cb078bf083 better api template & css modules example 2020-12-27 01:43:04 +02:00
Ben Elferink
40e22e0392 backend library now complete 2020-12-27 01:27:52 +02:00
Ben Elferink
96fcfceb88 expanded default layout for better dev experience 2020-12-27 01:21:05 +02:00
Ben Elferink
a2000e302a more info comments 2020-12-26 15:59:49 +02:00
Ben Elferink
b31db021cb demo video added to README
https://youtu.be/e81rqD94QtM
2020-12-25 19:38:43 +02:00
Ben Elferink
cc755d1deb added sample component, and sample style 2020-12-25 19:26:21 +02:00
Ben Elferink
5606f0203e refined server main file 2020-12-25 19:19:38 +02:00
Ben Elferink
1d481a17ae added new post example route 2020-12-25 19:18:56 +02:00
Ben Elferink
195b59c88d simplified controllers, added post example 2020-12-25 19:18:44 +02:00
Ben Elferink
cf21e5cc0f added base id to schema 2020-12-25 19:18:16 +02:00
Ben Elferink
14295ca8e5 renamed index.js to server.js 2020-12-23 18:36:20 +02:00
Ben Elferink
3dff3e6b96 Simplified some things 2020-12-23 15:44:35 +02:00
Ben Elferink
6c90e8dc28 api base structure 2020-12-23 00:54:43 +02:00
Ben Elferink
16df9f7e96 displayig node dependecies 2020-12-23 00:46:00 +02:00
Ben Elferink
04080c700b better layout 2020-12-23 00:45:37 +02:00
Ben Elferink
b82670a2ee adapted default styles 2020-12-22 20:55:46 +02:00
Ben Elferink
d36b840779 Update README.md 2020-12-22 15:13:59 +02:00
Ben Elferink
e50647784e removed default browsers from package 2020-12-22 02:27:23 +02:00
Ben Elferink
fedda7b932 example: route, controller, model 2020-12-22 02:11:43 +02:00
Ben Elferink
d389451be2 typo 2020-12-22 00:51:07 +02:00
Ben Elferink
0df0da4bf7 Update README.md 2020-12-22 00:29:56 +02:00
65 changed files with 21819 additions and 115 deletions

13
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
version: 2
updates:
- package-ecosystem: "npm"
directories:
- "/client"
- "/server"
schedule:
interval: "weekly"

31
.github/workflows/build-backend.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Build Backend (Server)
on:
pull_request:
branches:
- main
jobs:
build-backend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: './server'
strategy:
matrix:
node-version: [20.x]
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
clean: true
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: './server/package-lock.json'
- run: npm ci
- run: npm run build

31
.github/workflows/build-frontend.yml vendored Normal file
View File

@@ -0,0 +1,31 @@
name: Build Frontend (Client)
on:
pull_request:
branches:
- main
jobs:
build-frontend:
runs-on: ubuntu-latest
defaults:
run:
working-directory: './client'
strategy:
matrix:
node-version: [20.x]
steps:
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
clean: true
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: './client/package-lock.json'
- run: npm ci
- run: npm run build

16
.gitignore vendored
View File

@@ -1,2 +1,14 @@
/node_modules
.DS_Store
# dependencies
node_modules
.pnp
.pnp.js
# production
build
dist
# misc
.env
.DS_Store
*.log
*.pid

View File

@@ -1,49 +1,68 @@
# MERN Stack Template
<img src='https://raw.githubusercontent.com/BenElferink/mern-template/refs/heads/images/images/mern.jpeg' />
- **M** = [MongoDB](https://www.mongodb.com)
- **E** = [Express.js](https://expressjs.com)
- **R** = [React.js](https://reactjs.org)
- **N** = [Node.js](https://nodejs.org)
<br />
<br />
## How to use this template
# What is this template?
This template allows you to quick-start your Fullstack application using the MERN stack, it has a server setup with some basic authentication, and a client ready to communicate with the backend.
<img src='https://raw.githubusercontent.com/BenElferink/mern-template/refs/heads/images/images/preview.png' />
<br />
### STEP 1:
# How to use this template
Click ["Use this template"](https://github.com/belferink1996/MERN-template/generate) to generate a new repository.<br />
Then open your terminal and clone your repository:
### 1. Generate repository from template:
> cd ~/Desktop <br />
> git clone https://github.com/[your-user-name]/[your-repo-name].git
Click ["Use this template"](https://github.com/benelferink/mern-template/generate) to generate a
new repo, then open your terminal and clone your new repo.
<br />
```
git clone https://github.com/[your_user_name]/[your_repo_name].git
```
### STEP 2:
### 2. Install dependencies:
Go to your client folder (frontend), and set your workspace:
Go to the `server` folder, and run `install`.
> cd ~/Desktop/[your-repo-name]/client <br />
> npm install <br />
> npm start
```
cd ./server
npm i
```
<br />
Go to the `client` folder, and run `install`.
### STEP 3:
```
cd ./client
npm i
```
Prepare your MongoDB database,<br />
then go to your server folder (backend), and set your workspace:
### 3. Prepare MongoDB:
> cd ~/Desktop/[your-repo-name]/server <br />
> npm install <br />
Prepare your MongoDB database (using [Atlas](https://www.mongodb.com/cloud/atlas),
or [Community](<https://github.com/benelferink/mern-template/wiki/Install-MongoDB-Community-Server-(MacOS)>)). Then configure your database within `server/src/constants/index.js` (or `server/src/.env`), by configuring the `MONGO_URI` variable.
Set your database within `index.js`, then start server:
### 4. Start applications:
> npm start
Go to the `server` folder, and run `dev`.
<br />
```
cd ./server
npm run dev
```
### STEP 4: CODE !!!
Go to the `client` folder, and run `dev`.
```
cd ./client
npm run dev
```
### 5. Happy Coding !!!

1
client/.env.example Normal file
View File

@@ -0,0 +1 @@
REACT_APP_BACKEND_URL = ""

3
client/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/node_modules
.DS_Store
.eslintcache

18345
client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,17 +1,17 @@
{
"name": "client",
"name": "client-v2",
"version": "0.1.0",
"main": "index.js",
"private": true,
"scripts": {
"start": "react-scripts start"
"dev": "react-scripts start",
"build": "react-scripts build",
"start": "serve -s build"
},
"author": "Ben Elferink",
"license": "ISC",
"dependencies": {
"axios": "^0.21.0",
"react": "^17.0.1",
"react-dom": "^17.0.1",
"react-scripts": "^4.0.1"
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
@@ -24,5 +24,20 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^6.4.8",
"axios": "^1.8.4",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-scripts": "5.0.1",
"zustand": "^5.0.3"
},
"devDependencies": {
"@types/react": "^19.0.12",
"@types/react-dom": "^19.0.4",
"serve": "^14.2.4"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
client/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,38 @@
# Favicon SETUP
1. Go to [https://favicon.io](https://favicon.io)
2. Generate an icon pack
3. Drop all images in `/client/public/`
4. And configure these 2 files:
### manifest.json:
```
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src":"android-chrome-192x192.png",
"sizes":"192x192",
"type":"image/png"
},
{
"src":"android-chrome-512x512.png",
"sizes":"512x512",
"type":"image/png"
}
],
```
### index.html:
```
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="apple-touch-icon" type="image/png" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
```

View File

@@ -3,7 +3,19 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>MERN_STACK</title>
<!-- <meta name="author" content="" /> -->
<!-- <meta name="description" content="" /> -->
<!-- <meta name="keywords" content="" /> -->
<link rel="icon" type="image/x-icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="%PUBLIC_URL%/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-touch-icon.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>Web App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -0,0 +1,30 @@
{
"name": "Web Application",
"short_name": "Web App",
"description": "An awesome web application!",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"developer": {
"name": "",
"url": ""
}
}

3
client/public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@@ -0,0 +1,10 @@
export interface Account {
username: string
password: string
role: 'user' | 'admin'
}
export interface FormData {
username: Account['username']
password: Account['password']
}

View File

@@ -1,8 +0,0 @@
import React from 'react';
import './style/style.css';
function App() {
return <div>Hello World - React.js</div>;
}
export default App;

43
client/src/App.tsx Normal file
View File

@@ -0,0 +1,43 @@
import React, { Fragment } from 'react'
import { useAuth } from 'contexts/AuthContext'
import AuthModal from 'components/AuthModal'
import Header from 'components/Header'
import logo from 'assets/react.svg'
import 'styles/ReactWelcome.css'
const App = () => {
return (
<div className='App'>
<Header />
<ReactWelcome />
<LoggedInStatus />
<AuthModal />
</div>
)
}
const ReactWelcome = () => {
return (
<Fragment>
<img src={logo} className='ReactWelcome-logo' alt='logo' />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a className='ReactWelcome-link' href='https://reactjs.org' target='_blank' rel='noopener noreferrer'>
Learn React
</a>
</Fragment>
)
}
const LoggedInStatus = () => {
const { isLoggedIn, account } = useAuth()
if (isLoggedIn && !!account) {
return <p>Hey, {account.username}! I'm happy to let you know: you are authenticated!</p>
}
return <p>Don't forget to start your backend server, and then authenticate yourself.</p>
}
export default App

View File

@@ -1,6 +0,0 @@
import axios from 'axios';
const url = 'http://localhost:8080/';
// export const getSomething = () => axios.get(url + 'path');
// export const postSometing = (form) => axios.post(url + 'path', form);

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,92 @@
import React, { type ChangeEventHandler, Fragment, useState } from 'react'
import { useModalStore } from 'store/useModalStore'
import { useAuth } from 'contexts/AuthContext'
import { Dialog, DialogTitle, TextField, Button, CircularProgress } from '@mui/material'
import { type FormData } from '@types'
interface Props {}
const AuthModal: React.FC<Props> = () => {
const { login, register } = useAuth()
const { currentModal, setCurrentModal } = useModalStore()
const isRegisterMode = currentModal === 'REGISTER'
const isOpen = ['AUTH', 'LOGIN', 'REGISTER'].includes(currentModal)
const onClose = () => setCurrentModal('')
const [formData, setFormData] = useState<FormData>({ username: '', password: '' })
const [loading, setLoading] = useState(false)
const [error, setError] = useState('')
const handleChange: ChangeEventHandler<HTMLInputElement> = (e) => {
const { name, value } = e.target
setFormData((prev) => ({ ...prev, [name]: value }))
}
const clickSubmit = async () => {
setLoading(true)
setError('')
try {
isRegisterMode ? await register(formData) : await login(formData)
onClose()
} catch (error: any) {
setError(typeof error === 'string' ? error : JSON.stringify(error))
}
setLoading(false)
}
const isSubmitButtonDisabled = !formData['username'] || !formData['password']
return (
<Dialog open={isOpen} onClose={onClose}>
{isRegisterMode ? <DialogTitle>Create a new account</DialogTitle> : <DialogTitle>Login to your account</DialogTitle>}
<TextField
label='Username'
name='username'
type='text'
value={formData['username']}
onChange={handleChange}
variant='filled'
sx={{ mx: 2, my: 0.5 }}
required
/>
<TextField
label='Password'
name='password'
type='password'
value={formData['password']}
onChange={handleChange}
variant='filled'
sx={{ mx: 2, my: 0.5 }}
required
/>
{error && <span className='error'>{error}</span>}
{loading ? (
<center>
<CircularProgress color='inherit' />
</center>
) : isRegisterMode ? (
<Fragment>
<Button onClick={clickSubmit} disabled={isSubmitButtonDisabled}>
Register
</Button>
<Button onClick={() => setCurrentModal('LOGIN')}>I already have an account</Button>
</Fragment>
) : (
<Fragment>
<Button onClick={clickSubmit} disabled={isSubmitButtonDisabled}>
Login
</Button>
<Button onClick={() => setCurrentModal('REGISTER')}>I don't have an account</Button>
</Fragment>
)}
</Dialog>
)
}
export default AuthModal

View File

@@ -0,0 +1,70 @@
import React, { Fragment, type MouseEventHandler, useState } from 'react'
import { useModalStore } from 'store/useModalStore'
import { useAuth } from 'contexts/AuthContext'
import OnlineIndicator from 'components/OnlineIndicator'
import { AppBar, IconButton, Avatar, Popover, List, ListSubheader, ListItemButton } from '@mui/material'
interface Props {}
const Header: React.FC<Props> = () => {
const { isLoggedIn, account, logout } = useAuth()
const { setCurrentModal } = useModalStore()
const [anchorEl, setAnchorEl] = useState<(EventTarget & HTMLButtonElement) | null>(null)
const [popover, setPopover] = useState(false)
const openPopover: MouseEventHandler<HTMLButtonElement> = (e) => {
setPopover(true)
setAnchorEl(e.currentTarget)
}
const closePopover = () => {
setPopover(false)
setAnchorEl(null)
}
const clickLogin = () => {
setCurrentModal('LOGIN')
closePopover()
}
const clickRegister = () => {
setCurrentModal('REGISTER')
closePopover()
}
return (
<AppBar className='header' position='static'>
<h1>Web App</h1>
<IconButton onClick={openPopover}>
<OnlineIndicator online={isLoggedIn}>
<Avatar src={account?.username || ''} alt={account?.username || 'Guest'} />
</OnlineIndicator>
</IconButton>
<Popover
anchorEl={anchorEl}
open={popover}
onClose={closePopover}
anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
<List style={{ minWidth: '100px' }}>
<ListSubheader style={{ textAlign: 'center' }}>Hello, {account?.username || 'Guest'}</ListSubheader>
{isLoggedIn ? (
<ListItemButton onClick={logout}>Logout</ListItemButton>
) : (
<Fragment>
<ListItemButton onClick={clickLogin}>Login</ListItemButton>
<ListItemButton onClick={clickRegister}>Register</ListItemButton>
</Fragment>
)}
</List>
</Popover>
</AppBar>
)
}
export default Header

View File

@@ -0,0 +1,64 @@
import React from 'react'
import { styled } from '@mui/material/styles'
import { Badge, Avatar } from '@mui/material'
const StyledBadge = styled(Badge)(({ theme }) => ({
'& .MuiBadge-badge': {
backgroundColor: 'black',
color: 'black',
boxShadow: `0 0 0 2px ${theme.palette.background.paper}`,
'&::after': {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: '100%',
borderRadius: '50%',
animation: 'ripple 1.2s infinite ease-in-out',
border: '1px solid currentColor',
content: '""',
},
},
'@keyframes ripple': {
'0%': {
transform: 'scale(.8)',
opacity: 1,
},
'100%': {
transform: 'scale(2.4)',
opacity: 0,
},
},
}))
const OnlineBadge = styled(StyledBadge)({
'& .MuiBadge-badge': {
backgroundColor: 'var(--online)',
color: 'var(--online)',
},
})
const OfflineBadge = styled(StyledBadge)({
'& .MuiBadge-badge': {
backgroundColor: 'var(--offline)',
color: 'var(--offline)',
},
})
const OnlineIndicator = ({ online = false, children = <Avatar src='' alt='' /> }) => {
if (online) {
return (
<OnlineBadge variant='dot' overlap='circular' anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
{children}
</OnlineBadge>
)
}
return (
<OfflineBadge variant='dot' overlap='circular' anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }}>
{children}
</OfflineBadge>
)
}
export default OnlineIndicator

View File

@@ -0,0 +1,4 @@
// api url (where your server is hosted at)
const BACKEND_URL = process.env.REACT_APP_BACKEND_URL || 'http://localhost:8080'
export { BACKEND_URL }

View File

@@ -0,0 +1,113 @@
import React, { createContext, useContext, useState, useEffect, type PropsWithChildren, useMemo } from 'react'
import axios from 'utils/axios'
import { type FormData, type Account } from '@types'
interface Context {
token: string | null
account: Account | null
isLoggedIn: boolean
register: (payload: FormData) => Promise<any>
login: (payload: FormData) => Promise<any>
logout: () => void
}
const initContext: Context = {
token: null,
account: null,
isLoggedIn: false,
register: async () => {},
login: async () => {},
logout: () => {},
}
// init context
const AuthContext = createContext(initContext)
const { Provider } = AuthContext
// export the consumer
export const useAuth = () => useContext(AuthContext)
// export the provider
export const AuthProvider = ({ children }: PropsWithChildren) => {
const [token, setToken] = useState(localStorage.getItem('token') || initContext.token)
const [account, setAccount] = useState(initContext.account)
const [isLoggedIn, setIsLoggedIn] = useState(initContext.isLoggedIn)
const register = (formData: FormData) => {
return new Promise((resolve, reject) => {
axios
.post('/auth/register', formData)
.then(({ data: { data: accountData, token: accessToken } }) => {
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
resolve(true)
})
.catch((error) => {
reject(error?.response?.data?.message || error.message)
})
})
}
const login = (formData: FormData) => {
return new Promise((resolve, reject) => {
axios
.post('/auth/login', formData)
.then(({ data: { data: accountData, token: accessToken } }) => {
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
resolve(true)
})
.catch((error) => {
reject(error?.response?.data?.message || error.message)
})
})
}
const logout = () => {
setIsLoggedIn(false)
setAccount(null)
setToken(null)
}
const loginWithToken = async () => {
try {
const {
data: { data: accountData, token: accessToken },
} = await axios.get('/auth/login', {
headers: {
authorization: `Bearer ${token}`,
},
})
setAccount(accountData)
setToken(accessToken)
setIsLoggedIn(true)
} catch (error: any) {
console.error(error)
if (error?.response?.statusCode === 401) setToken(null)
}
}
// This side effect keeps local storage updated with recent token value,
// making sure it can be re-used upon refresh or re-open browser
useEffect(() => {
if (token) {
localStorage.setItem('token', token)
} else {
localStorage.removeItem('token')
}
}, [token])
// This side effect runs only if we have a token, but no account or logged-in boolean.
// This "if" statement is "true" only when refreshed, or re-opened the browser,
// if true, it will then ask the backend for the account information (and will get them if the token hasn't expired)
useEffect(() => {
if (!isLoggedIn && !account && token) loginWithToken()
}, [isLoggedIn, account, token]) // eslint-disable-line react-hooks/exhaustive-deps
const value = useMemo(() => ({ token, account, isLoggedIn, register, login, logout }), [token, account, isLoggedIn])
return <Provider value={value}>{children}</Provider>
}

View File

@@ -0,0 +1,19 @@
const getQueryPayload = (queryStr?: string) => {
if (!queryStr) {
console.warn('query string is not defined')
return {}
}
const queryObj: Record<string, string> = {}
const queryArr = (queryStr[0] === '?' ? queryStr.substring(1, queryStr.length) : queryStr).split('&')
queryArr.forEach((str) => {
const [key, val] = str.split('=')
queryObj[key] = val
})
return queryObj
}
export default getQueryPayload

View File

@@ -0,0 +1,13 @@
const getTokenPayload = (token?: string) => {
if (!token) {
console.warn('token is not defined')
return {}
}
const informativePart = token.split('.')[1]
const payload = JSON.parse(window.atob(informativePart))
return payload
}
export default getTokenPayload

View File

@@ -0,0 +1,42 @@
import { useState, useEffect } from 'react'
/*
How to use this hook:
import useLocalStorage from './hooks/useLocalStorage';
function App() {
const [example, setExample] = useLocalStorage('example_key', { example: 'anything as default' });
return ();
};
*/
const getLsVal = (key: string, defaultValue: any) => {
const storedStr = localStorage.getItem(key) || ''
if (!!storedStr) {
return JSON.parse(storedStr)
} else {
return defaultValue
}
}
const setLsVal = (key: string, value: any) => {
if (value !== undefined && value !== null) {
const str = JSON.stringify(value)
localStorage.setItem(key, str)
}
}
const useLocalStorage = (key: string, defaultValue: any = null) => {
const [value, setValue] = useState(getLsVal(key, defaultValue))
useEffect(() => setLsVal(key, value), [key, value])
return [value, setValue]
}
export default useLocalStorage

View File

@@ -0,0 +1,30 @@
import { useState, useEffect } from 'react'
/*
How to use this hook:
import useMediaQuery from './hooks/useMediaQuery';
function App() {
const isMobile = useMediaQuery('(max-width: 768px)');
return ();
};
*/
const useMediaQuery = (query: string = '(max-width: 768px)') => {
const [matches, setMatches] = useState(window.matchMedia(query).matches)
useEffect(() => {
const media = window.matchMedia(query)
const listener = () => setMatches(media.matches)
media.addListener(listener)
return () => media.removeListener(listener)
}, [query])
return matches
}
export default useMediaQuery

View File

@@ -1,10 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
document.getElementById('root')
);

18
client/src/index.tsx Normal file
View File

@@ -0,0 +1,18 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import CssBaseline from '@mui/material/CssBaseline'
import { AuthProvider } from 'contexts/AuthContext'
import App from 'App'
import 'styles/index.css'
const element = document.getElementById('root') as HTMLElement
const root = ReactDOM.createRoot(element)
root.render(
<React.StrictMode>
<AuthProvider>
<CssBaseline />
<App />
</AuthProvider>
</React.StrictMode>
)

1
client/src/react-app-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="react-scripts" />

View File

@@ -0,0 +1,11 @@
import { create } from 'zustand'
interface StoreState {
currentModal: string
setCurrentModal: (str: string) => void
}
export const useModalStore = create<StoreState>((set) => ({
currentModal: '',
setCurrentModal: (str) => set({ currentModal: str }),
}))

View File

@@ -1,9 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: Arial, Helvetica, sans-serif;
}

View File

@@ -0,0 +1,23 @@
.ReactWelcome-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.ReactWelcome-logo {
animation: ReactWelcome-logo-spin infinite 20s linear;
}
}
.ReactWelcome-link {
color: #61dafb;
}
@keyframes ReactWelcome-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -0,0 +1,63 @@
:root {
--online: #44b700;
--offline: #b74400;
}
body {
margin: 0;
font-size: 16px;
font-family: -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;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
.App {
width: 100vw;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.header {
width: 100% !important;
padding: 0 1rem !important;
background-color: whitesmoke !important;
color: black !important;
display: flex !important;
flex-direction: row !important;
align-items: center !important;
justify-content: space-between !important;
}
.error {
margin: 0.5rem;
color: red;
text-align: center;
}
/*
Extra Small Devices, Phones
@media only screen and (min-width: 480px) {}
*/
/*
Small Devices, Tablets
@media only screen and (min-width: 768px) {}
*/
/*
Medium Devices, Desktops
@media only screen and (min-width: 992px) {}
*/
/*
Large Devices, Wide Screens
@media only screen and (min-width: 1200px) {}
*/

View File

@@ -0,0 +1,8 @@
import Axios from 'axios'
import { BACKEND_URL } from '../constants'
const axios = Axios.create({
baseURL: BACKEND_URL,
})
export default axios

27
client/tsconfig.json Normal file
View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "es5", // Specify ECMAScript target version
"lib": ["dom", "dom.iterable", "esnext"], // List of library files to be included in the compilation
"allowJs": true, // Allow JavaScript files to be compiled
"skipLibCheck": true, // Skip type checking of all declaration files
"esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
"allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
"strict": true, // Enable all strict type checking options
"forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
"module": "esnext", // Specify module code generation
"moduleResolution": "node", // Resolve modules using Node.js style
"isolatedModules": true, // Unconditionally emit imports for unresolved files
"resolveJsonModule": true, // Include modules imported with .json extension
"noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
"jsx": "react", // Support JSX in .tsx files
"sourceMap": true, // Generate corrresponding .map file
"declaration": true, // Generate corresponding .d.ts file
"noUnusedLocals": true, // Report errors on unused locals
"noUnusedParameters": true, // Report errors on unused parameters
"incremental": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk
"noFallthroughCasesInSwitch": true, // Report errors for fallthrough cases in switch statement
"baseUrl": "src"
},
"include": ["src"],
"exclude": ["node_modules", "build"]
}

16
package.json Normal file
View File

@@ -0,0 +1,16 @@
{
"name": "mern-application",
"version": "0.1.0",
"private": true,
"homepage": "/",
"author": "Ben Elferink <ben.elferink@icloud.com> (https://github.com/BenElferink)",
"repository": {
"type": "git",
"url": "https://github.com/BenElferink/mern-template.git"
},
"bugs": {
"url": "https://github.com/BenElferink/mern-template/issues",
"email": "ben.elferink@icloud.com"
},
"scripts": {}
}

View File

@@ -1 +1,3 @@
CONNECTIONS_URL = "URL"
PORT = ""
MONGO_URI = ""
JWT_SECRET = ""

3
server/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/node_modules
.DS_Store
.env

View File

@@ -1,30 +0,0 @@
import mongoose from 'mongoose';
import express from 'express';
import dotenv from 'dotenv';
import cors from 'cors';
// initialize app
const app = express();
dotenv.config();
// connect to db ---> if you want to connect to cloud server: edit "CONNECTION_URL" in -> .env file
const DB_NAME = 'testDB'; // if you want to use local server: edit this "DB_NAME" (and remove the "CONNECTION_URL" from -> .env file)
const CONNECTION_URL = process.env.CONNECTION_URL || `mongodb://localhost:27017/${DB_NAME}`;
const PORT = process.env.PORT || 8080; // 8080 === development port
const DEPRECATED_FIX = { useNewUrlParser: true, useUnifiedTopology: true }; // change this with (possible) warnings on first connection
mongoose
.connect(CONNECTION_URL, DEPRECATED_FIX)
.then(() => console.log('✅ MongoDB connected'))
.then(() => app.listen(PORT, () => console.log(`✅ Listening on port: ${PORT}`)))
.catch((error) => console.log(`${error}`));
mongoose.connection.on('error', (err) => console.log(`❌ MongoDB: ${err}`));
// middlewares
app.use(express.json()); // body parser
app.use(cors()); // enables requests
// routes
app.get('/', (req, res) => res.send('Hello World - Express.js'));
// app.use('/', IMPORTED_ROUTES);

2101
server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,29 @@
{
"name": "server",
"version": "0.1.0",
"main": "index.js",
"type": "module",
"private": true,
"type": "commonjs",
"main": "src/index.ts",
"scripts": {
"start": "nodemon index.js"
"dev": "npx tsx src/index.ts",
"build": "rm -rf dist && npx tsc",
"start": "node dist/index.js"
},
"author": "Ben Elferink",
"license": "ISC",
"dependencies": {
"bcrypt": "^5.1.1",
"cors": "^2.8.5",
"dotenv": "^8.2.0",
"express": "^4.17.1",
"mongoose": "^5.11.8",
"nodemon": "^2.0.6"
"dotenv": "^16.4.7",
"express": "^4.21.2",
"joi": "^17.13.3",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.15.1"
},
"devDependencies": {
"@types/bcrypt": "^5.0.2",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.0",
"@types/jsonwebtoken": "^9.0.9",
"@types/node": "^22.13.12",
"typescript": "^5.8.2"
}
}

10
server/src/@types/express.d.ts vendored Normal file
View File

@@ -0,0 +1,10 @@
import 'express'
import jwt from 'jsonwebtoken'
declare global {
namespace Express {
interface Request {
auth?: jwt.JwtPayload // { uid: string; role: string }
}
}
}

View File

@@ -0,0 +1,5 @@
export interface Account {
username: string
password: string
role: 'user' | 'admin'
}

View File

@@ -0,0 +1,11 @@
const ORIGIN = '*'
const PORT = process.env.PORT || 8080
// For "MongoDB Atlas": edit MONGO_URI in -> .env file
// For "MongoDB Community Server": edit <DB_NAME> in -> MONGO_URI below
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/<DB_NAME>'
const MONGO_OPTIONS = {}
const JWT_SECRET = process.env.JWT_SECRET || 'unsafe_secret'
export { ORIGIN, PORT, MONGO_URI, MONGO_OPTIONS, JWT_SECRET }

View File

@@ -0,0 +1,32 @@
import { type RequestHandler } from 'express'
import jwt from '../../utils/jwt'
import Account from '../../models/Account'
const loginWithToken: RequestHandler = async (req, res, next) => {
try {
const { uid } = req.auth || {}
// Get account from DB, password is not verified because we're already token-authorized at this point
const account = await Account.findOne({ _id: uid }).select('-password')
if (!account) {
return next({
statusCode: 400,
message: 'Bad credentials',
})
}
// Generate access token
const token = jwt.signToken({ uid: account._id, role: account.role })
res.status(200).json({
message: 'Succesfully got account',
data: account,
token,
})
} catch (error) {
next(error)
}
}
export default loginWithToken

View File

@@ -0,0 +1,59 @@
import { type RequestHandler } from 'express'
import joi from '../../utils/joi'
import jwt from '../../utils/jwt'
import crypt from '../../utils/crypt'
import Account from '../../models/Account'
const login: RequestHandler = async (req, res, next) => {
try {
const validationError = await joi.validate(
{
username: joi.instance.string().required(),
password: joi.instance.string().required(),
},
req.body
)
if (validationError) {
return next(validationError)
}
const { username, password } = req.body
// Get account from DB, and verify existance
const account = await Account.findOne({ username })
if (!account) {
return next({
statusCode: 400,
message: 'Bad credentials',
})
}
// Verify password hash
const passOk = crypt.validate(password, account.password)
if (!passOk) {
return next({
statusCode: 400,
message: 'Bad credentials',
})
}
// Generate access token
const token = jwt.signToken({ uid: account._id, role: account.role })
// Remove password from response data
const { password: _, ...accountData } = account.toObject()
res.status(200).json({
message: 'Succesfully logged-in',
data: accountData,
token,
})
} catch (error) {
next(error)
}
}
export default login

View File

@@ -0,0 +1,56 @@
import { type RequestHandler } from 'express'
import joi from '../../utils/joi'
import jwt from '../../utils/jwt'
import crypt from '../../utils/crypt'
import Account from '../../models/Account'
const register: RequestHandler = async (req, res, next) => {
try {
const validationError = await joi.validate(
{
username: joi.instance.string().required(),
password: joi.instance.string().required(),
},
req.body
)
if (validationError) {
return next(validationError)
}
const { username, password } = req.body
// Verify account username as unique
const found = await Account.findOne({ username })
if (found) {
return next({
statusCode: 400,
message: 'An account already exists with that "username"',
})
}
// Encrypt password
const hash = crypt.hash(password)
// Create account
const account = new Account({ username, password: hash })
await account.save()
// Generate access token
const token = jwt.signToken({ uid: account._id, role: account.role })
// Exclude password from response
const { password: _, ...data } = account.toObject()
res.status(201).json({
message: 'Succesfully registered',
data,
token,
})
} catch (error) {
next(error)
}
}
export default register

28
server/src/index.ts Normal file
View File

@@ -0,0 +1,28 @@
import dotenv from 'dotenv'
dotenv.config()
import app from './utils/app' // (server)
import mongo from './utils/mongo' // (database)
import { PORT } from './constants/index'
import authRoutes from './routes/auth'
const bootstrap = async () => {
await mongo.connect()
app.get('/', (req, res) => {
res.status(200).send('Hello, world!')
})
app.get('/healthz', (req, res) => {
res.status(204).end()
})
app.use('/auth', authRoutes)
// add rest of routes here...
app.listen(PORT, () => {
console.log(`✅ Server is listening on port: ${PORT}`)
})
}
bootstrap()

View File

@@ -0,0 +1,35 @@
import { type RequestHandler } from 'express'
import jwt from '../utils/jwt'
const checkBearerToken: RequestHandler = (req, res, next) => {
try {
const token = req.headers.authorization?.split(' ')[1]
if (!token) {
return next({
statusCode: 400,
message: 'Token not provided',
})
}
const auth = jwt.verifyToken(token)
if (!auth) {
return next({
statusCode: 401,
message: 'Invalid token',
})
}
req.auth = typeof auth === 'string' ? JSON.parse(auth) : auth
next()
} catch (error) {
next({
statusCode: 401,
message: 'Invalid token',
})
}
}
export default checkBearerToken

View File

@@ -0,0 +1,12 @@
import { type NextFunction, type Request, type Response } from 'express'
const errorHandler = (error: any, req: Request, res: Response, next: NextFunction) => {
const { statusCode = 500, message = 'Internal server error', ...rest } = error
res.status(statusCode).json({
message,
...rest,
})
}
export default errorHandler

View File

@@ -0,0 +1,39 @@
import { type Document, model, Schema } from 'mongoose'
import { type Account } from '../@types'
interface I extends Document, Account {}
const instance = new Schema<I>(
{
/*
document ID is set by default via MongoDB - the next line is deprecated!
_id: mongoose.Schema.Types.ObjectId,
*/
username: {
type: String,
required: true,
lowercase: true,
unique: true,
},
password: {
type: String,
required: true,
},
role: {
type: String,
required: true,
enum: ['user', 'admin'],
default: 'user',
},
},
{
timestamps: true,
}
)
// NOTE! use a singular model name, mongoose automatically creates a collection like so:
// model: 'Account' === collection: 'accounts'
const modelName = 'Account'
export default model<I>(modelName, instance)

20
server/src/routes/auth.ts Normal file
View File

@@ -0,0 +1,20 @@
import express from 'express'
import checkBearerToken from '../middlewares/check-bearer-token'
import errorHandler from '../middlewares/error-handler'
import register from '../controllers/auth/register'
import login from '../controllers/auth/login'
import loginWithToken from '../controllers/auth/login-with-token'
// initialize router
const router = express.Router()
// POST at route: http://localhost:8080/auth/register
router.post('/register', [], register, errorHandler)
// POST at path: http://localhost:8080/auth/login
router.post('/login', [], login, errorHandler)
// GET at path: http://localhost:8080/auth/account
router.get('/login', [checkBearerToken], loginWithToken, errorHandler)
export default router

13
server/src/utils/app.ts Normal file
View File

@@ -0,0 +1,13 @@
import express from 'express'
import cors from 'cors'
import { ORIGIN } from '../constants/index'
// initialize app
const app = express()
// middlewares
app.use(cors({ origin: ORIGIN }))
app.use(express.json()) // body parser
app.use(express.urlencoded({ extended: false })) // url parser
export default app

22
server/src/utils/crypt.ts Normal file
View File

@@ -0,0 +1,22 @@
import bcrypt from 'bcrypt'
class Crypt {
instance: typeof bcrypt = bcrypt
constructor() {}
async hash(value: string) {
const salt = await this.instance.genSalt(10)
const hash = await this.instance.hash(value, salt)
return hash
}
async validate(value: string, hash: string) {
const isOk = await bcrypt.compare(value, hash)
return isOk
}
}
export default new Crypt()

22
server/src/utils/joi.ts Normal file
View File

@@ -0,0 +1,22 @@
import joi from 'joi'
class Joi {
instance: typeof joi = joi
constructor() {}
async validate(schema: Record<string, any>, body: Record<string, any>) {
try {
await this.instance.object(schema).validateAsync(body)
} catch (error: any) {
console.log('❌ Joi validation error:', error.message)
return {
statusCode: 400,
message: error.message,
}
}
}
}
export default new Joi()

25
server/src/utils/jwt.ts Normal file
View File

@@ -0,0 +1,25 @@
import jsonwebtoken from 'jsonwebtoken'
import { JWT_SECRET } from '../constants/index'
class JWT {
instance: typeof jsonwebtoken = jsonwebtoken
secret: string
constructor() {
this.secret = JWT_SECRET
}
signToken(payload: Record<string, any>, expiresIn: jsonwebtoken.SignOptions['expiresIn'] = '12h') {
const token = this.instance.sign(payload, JWT_SECRET, { expiresIn })
return token
}
verifyToken(token: string) {
const auth = this.instance.verify(token, JWT_SECRET)
return auth
}
}
export default new JWT()

37
server/src/utils/mongo.ts Normal file
View File

@@ -0,0 +1,37 @@
import mongoose from 'mongoose'
import { MONGO_URI, MONGO_OPTIONS } from '../constants/index'
class Mongo {
instance: typeof mongoose = mongoose
mongoUri: string
mongoOptions: mongoose.ConnectOptions
isConnected: boolean
constructor() {
this.mongoUri = MONGO_URI
this.mongoOptions = MONGO_OPTIONS
this.isConnected = false
}
async connect() {
if (this.isConnected) return
try {
console.log('⏳ Connecting to MongoDB')
const db = await this.instance.connect(this.mongoUri, this.mongoOptions)
const connection = db.connection
this.isConnected = connection.readyState === 1
if (this.isConnected) console.log('✅ MongoDB connected')
connection.on('connected', () => console.log('✅ MongoDB connected')) // re-connected
connection.on('disconnected', () => console.log('❌ MongoDB disconnected')) // disconnected
connection.on('error', (error) => console.log('❌ MongoDB connection error', error)) // listen for errors during the session
} catch (error: any) {
console.log('❌ MongoDB connection error:', error.message)
}
}
}
export default new Mongo()

15
server/tsconfig.json Normal file
View File

@@ -0,0 +1,15 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS",
"moduleResolution": "node",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}