feat: V2 with TypeScript
This commit is contained in:
16
.gitignore
vendored
16
.gitignore
vendored
@@ -1,2 +1,14 @@
|
||||
/node_modules
|
||||
.DS_Store
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
|
||||
# production
|
||||
build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.env
|
||||
.DS_Store
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
73
README.md
73
README.md
@@ -1,73 +1,70 @@
|
||||
# MERN Stack Template
|
||||
|
||||
<img src='https://github.com/belferink1996/MERN-template/blob/images/images/mern.jpeg' alt='MERN banner' width='700' />
|
||||
<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 />
|
||||
|
||||
# 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.<br />
|
||||
I have attempted to use the best practices for both ends, which should make it easy for any advanced/new developer to use, and perhaps learn from.
|
||||
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://github.com/belferink1996/MERN-template/blob/images/images/Screen%20Shot%202021-09-29%20at%2011.08.23.png' width='700' />
|
||||
<img src='https://raw.githubusercontent.com/BenElferink/mern-template/refs/heads/images/images/preview.png' />
|
||||
|
||||
<br />
|
||||
<br />
|
||||
|
||||
# How to use this template
|
||||
|
||||
[📀 Demo video](https://youtu.be/N2pvvkyoS68)
|
||||
|
||||
### STEP 1:
|
||||
### 1. Generate repository from template:
|
||||
|
||||
Click ["Use this template"](https://github.com/benelferink/MERN-template/generate) to generate a
|
||||
new repository.<br />
|
||||
Then open your terminal and clone your repository:
|
||||
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.
|
||||
|
||||
> cd ~/Desktop <br />
|
||||
> git clone https://github.com/[your-user-name]/[your-repo-name].git
|
||||
```
|
||||
git clone https://github.com/[your_user_name]/[your_repo_name].git
|
||||
```
|
||||
|
||||
<br />
|
||||
### 2. Install dependencies:
|
||||
|
||||
### STEP 2:
|
||||
Go to the `server` folder, and run `install`.
|
||||
|
||||
Go to the root of your repository's folder, and install all dependecies:
|
||||
```
|
||||
cd ./server
|
||||
npm i
|
||||
```
|
||||
|
||||
> cd ~/Desktop/[your-repo-name]<br />
|
||||
> npm install
|
||||
Go to the `client` folder, and run `install`.
|
||||
|
||||
<br />
|
||||
```
|
||||
cd ./client
|
||||
npm i
|
||||
```
|
||||
|
||||
### STEP 3:
|
||||
### 3. Prepare MongoDB:
|
||||
|
||||
Prepare your MongoDB database ([atlas](https://www.mongodb.com/cloud/atlas),
|
||||
[community](<https://github.com/benelferink/MERN-template/wiki/Install-MongoDB-Community-Server-(MacOS)>)).<br />
|
||||
Then configure your database within `server/constants/index.js`, by configuring the `MONGO_URI` variable.
|
||||
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.
|
||||
|
||||
<br />
|
||||
### 4. Start applications:
|
||||
|
||||
### STEP 4: CODE !!!
|
||||
Go to the `server` folder, and run `dev`.
|
||||
|
||||
<br />
|
||||
<br />
|
||||
```
|
||||
cd ./server
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### To run the client and/or the server, you can do any of the following:
|
||||
Go to the `client` folder, and run `dev`.
|
||||
|
||||
From the `root` of your project run:
|
||||
> npm start
|
||||
```
|
||||
cd ./client
|
||||
npm run dev
|
||||
```
|
||||
|
||||
#### OR
|
||||
|
||||
Open terminal #1 (backend)
|
||||
> cd ./server<br />
|
||||
> npm start
|
||||
|
||||
Open terminal #2 (frontend)
|
||||
> cd ./client<br />
|
||||
> npm start
|
||||
### 5. Happy Coding !!!
|
||||
|
||||
21
client/.gitignore
vendored
21
client/.gitignore
vendored
@@ -1,21 +0,0 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
41424
client/package-lock.json
generated
41424
client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,27 +1,16 @@
|
||||
{
|
||||
"name": "client",
|
||||
"name": "client-v2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"license": "ISC",
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"dev": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.5.0",
|
||||
"@emotion/styled": "^11.3.0",
|
||||
"@fontsource/roboto": "^4.5.1",
|
||||
"@mui/material": "^5.0.6",
|
||||
"axios": "^0.24.0",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-scripts": "^4.0.3"
|
||||
"start": "serve -s build"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app"
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
@@ -35,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.2.0",
|
||||
"axios": "^1.7.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-scripts": "5.0.1",
|
||||
"zustand": "^5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.0.1",
|
||||
"@types/react-dom": "^19.0.2",
|
||||
"serve": "^14.2.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,15 @@
|
||||
<!-- <meta name="author" content="" /> -->
|
||||
<!-- <meta name="description" content="" /> -->
|
||||
<!-- <meta name="keywords" content="" /> -->
|
||||
<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" 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>
|
||||
|
||||
@@ -9,14 +9,14 @@
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src":"/android-chrome-192x192.png",
|
||||
"sizes":"192x192",
|
||||
"type":"image/png"
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src":"/android-chrome-512x512.png",
|
||||
"sizes":"512x512",
|
||||
"type":"image/png"
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
10
client/src/@types/index.ts
Normal file
10
client/src/@types/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Account {
|
||||
username: string
|
||||
password: string
|
||||
role: 'user' | 'admin'
|
||||
}
|
||||
|
||||
export interface FormData {
|
||||
username: Account['username']
|
||||
password: Account['password']
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import {useAuth} from './contexts/AuthContext'
|
||||
import Header from './components/Header'
|
||||
|
||||
export default function App() {
|
||||
const {isLoggedIn} = useAuth()
|
||||
|
||||
return (
|
||||
<div className='App'>
|
||||
<Header />
|
||||
|
||||
{isLoggedIn ? <LoggedInText /> : <LoggedOutText />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const LoggedInText = () => {
|
||||
const {account} = useAuth()
|
||||
|
||||
return <p>Hey, {account.username}! I'm happy to let you know: you are authenticated!</p>
|
||||
}
|
||||
|
||||
const LoggedOutText = () => (
|
||||
<p>Don't forget to start your backend server, then authenticate yourself.</p>
|
||||
)
|
||||
43
client/src/App.tsx
Normal file
43
client/src/App.tsx
Normal 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
|
||||
1
client/src/assets/react.svg
Normal file
1
client/src/assets/react.svg
Normal 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 |
@@ -1,121 +0,0 @@
|
||||
import {Fragment, useState} from 'react'
|
||||
import {Dialog, DialogTitle, TextField, Button, CircularProgress} from '@mui/material'
|
||||
import {useAuth} from '../contexts/AuthContext'
|
||||
|
||||
const textFieldSx = {mx: 2, my: 0.5}
|
||||
|
||||
export default function AuthModal({open, close, isRegisterMode, toggleRegister}) {
|
||||
const {login, register} = useAuth()
|
||||
|
||||
const [formData, setFormData] = useState({})
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
const handleChange = (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)
|
||||
close()
|
||||
} catch (error) {
|
||||
setError(error)
|
||||
}
|
||||
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
const disabledLoginButton = !formData['username'] || !formData['password']
|
||||
const disabledRegisterButton = !formData['username'] || !formData['password']
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={close}>
|
||||
{isRegisterMode ? (
|
||||
<RegisterForm formData={formData} handleChange={handleChange} />
|
||||
) : (
|
||||
<LoginForm formData={formData} handleChange={handleChange} />
|
||||
)}
|
||||
|
||||
{error && <span className='error'>{error}</span>}
|
||||
|
||||
{loading ? (
|
||||
<center>
|
||||
<CircularProgress color='inherit' />
|
||||
</center>
|
||||
) : (
|
||||
<Button
|
||||
onClick={clickSubmit}
|
||||
disabled={isRegisterMode ? disabledRegisterButton : disabledLoginButton}>
|
||||
{isRegisterMode ? 'Register' : 'Login'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button onClick={toggleRegister}>
|
||||
{isRegisterMode ? 'I already have an account' : "I don't have an account"}
|
||||
</Button>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
function LoginForm({formData, handleChange}) {
|
||||
return (
|
||||
<Fragment>
|
||||
<DialogTitle>Login to your account</DialogTitle>
|
||||
|
||||
<TextField
|
||||
label='Username'
|
||||
name='username'
|
||||
type='text'
|
||||
value={formData['username'] || ''}
|
||||
onChange={handleChange}
|
||||
variant='filled'
|
||||
sx={textFieldSx}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label='Password'
|
||||
name='password'
|
||||
type='password'
|
||||
value={formData['password'] || ''}
|
||||
onChange={handleChange}
|
||||
variant='filled'
|
||||
sx={textFieldSx}
|
||||
required
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
function RegisterForm({formData, handleChange}) {
|
||||
return (
|
||||
<Fragment>
|
||||
<DialogTitle>Create a new account</DialogTitle>
|
||||
|
||||
<TextField
|
||||
label='Username'
|
||||
name='username'
|
||||
type='text'
|
||||
value={formData['username'] || ''}
|
||||
onChange={handleChange}
|
||||
variant='filled'
|
||||
sx={textFieldSx}
|
||||
required
|
||||
/>
|
||||
<TextField
|
||||
label='Password'
|
||||
name='password'
|
||||
type='password'
|
||||
value={formData['password'] || ''}
|
||||
onChange={handleChange}
|
||||
variant='filled'
|
||||
sx={textFieldSx}
|
||||
required
|
||||
/>
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
92
client/src/components/AuthModal.tsx
Normal file
92
client/src/components/AuthModal.tsx
Normal 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
|
||||
@@ -1,85 +0,0 @@
|
||||
import {Fragment, useState} from 'react'
|
||||
import {
|
||||
AppBar,
|
||||
IconButton,
|
||||
Avatar,
|
||||
Popover,
|
||||
List,
|
||||
ListSubheader,
|
||||
ListItemButton,
|
||||
} from '@mui/material'
|
||||
import OnlineIndicator from './OnlineIndicator'
|
||||
import AuthModal from './AuthModal'
|
||||
import {useAuth} from '../contexts/AuthContext'
|
||||
|
||||
export default function Header() {
|
||||
const {isLoggedIn, account, logout} = useAuth()
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState(null)
|
||||
const [popover, setPopover] = useState(false)
|
||||
const [authModal, setAuthModal] = useState(false)
|
||||
const [register, setRegister] = useState(false)
|
||||
|
||||
const openPopover = (e) => {
|
||||
setPopover(true)
|
||||
setAnchorEl(e.currentTarget)
|
||||
}
|
||||
|
||||
const closePopover = () => {
|
||||
setPopover(false)
|
||||
setAnchorEl(null)
|
||||
}
|
||||
|
||||
const clickLogin = () => {
|
||||
setRegister(false)
|
||||
setAuthModal(true)
|
||||
closePopover()
|
||||
}
|
||||
|
||||
const clickRegister = () => {
|
||||
setRegister(true)
|
||||
setAuthModal(true)
|
||||
closePopover()
|
||||
}
|
||||
|
||||
return (
|
||||
<AppBar className='header' position='static'>
|
||||
<h1>Web App</h1>
|
||||
|
||||
<IconButton onClick={openPopover}>
|
||||
<OnlineIndicator online={isLoggedIn}>
|
||||
<Avatar src={account?.username || ''} alt={account?.username || ''} />
|
||||
</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, {isLoggedIn ? account.username : 'Guest'}
|
||||
</ListSubheader>
|
||||
|
||||
{isLoggedIn ? (
|
||||
<ListItemButton onClick={logout}>Logout</ListItemButton>
|
||||
) : (
|
||||
<Fragment>
|
||||
<ListItemButton onClick={clickLogin}>Login</ListItemButton>
|
||||
<ListItemButton onClick={clickRegister}>Reigster</ListItemButton>
|
||||
</Fragment>
|
||||
)}
|
||||
</List>
|
||||
</Popover>
|
||||
|
||||
<AuthModal
|
||||
open={authModal}
|
||||
close={() => setAuthModal(false)}
|
||||
isRegisterMode={register}
|
||||
toggleRegister={() => setRegister((prev) => !prev)}
|
||||
/>
|
||||
</AppBar>
|
||||
)
|
||||
}
|
||||
70
client/src/components/Header.tsx
Normal file
70
client/src/components/Header.tsx
Normal 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
|
||||
@@ -1,7 +1,8 @@
|
||||
import {styled} from '@mui/material/styles'
|
||||
import {Badge, Avatar} from '@mui/material'
|
||||
import React from 'react'
|
||||
import { styled } from '@mui/material/styles'
|
||||
import { Badge, Avatar } from '@mui/material'
|
||||
|
||||
const StyledBadge = styled(Badge)(({theme}) => ({
|
||||
const StyledBadge = styled(Badge)(({ theme }) => ({
|
||||
'& .MuiBadge-badge': {
|
||||
backgroundColor: 'black',
|
||||
color: 'black',
|
||||
@@ -30,34 +31,34 @@ const StyledBadge = styled(Badge)(({theme}) => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const OnlineBadge = styled(StyledBadge)(({theme}) => ({
|
||||
const OnlineBadge = styled(StyledBadge)({
|
||||
'& .MuiBadge-badge': {
|
||||
backgroundColor: 'var(--online)',
|
||||
color: 'var(--online)',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
const OfflineBadge = styled(StyledBadge)(({theme}) => ({
|
||||
const OfflineBadge = styled(StyledBadge)({
|
||||
'& .MuiBadge-badge': {
|
||||
backgroundColor: 'var(--offline)',
|
||||
color: 'var(--offline)',
|
||||
},
|
||||
}))
|
||||
})
|
||||
|
||||
export default function OnlineIndicator({online = false, children = <Avatar src='' alt='' />}) {
|
||||
return online ? (
|
||||
<OnlineBadge
|
||||
variant='dot'
|
||||
overlap='circular'
|
||||
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
|
||||
{children}
|
||||
</OnlineBadge>
|
||||
) : (
|
||||
<OfflineBadge
|
||||
variant='dot'
|
||||
overlap='circular'
|
||||
anchorOrigin={{vertical: 'bottom', horizontal: 'right'}}>
|
||||
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
|
||||
@@ -1,6 +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,
|
||||
}
|
||||
export { BACKEND_URL }
|
||||
@@ -1,61 +1,69 @@
|
||||
import { createContext, useContext, useState, useEffect } from 'react'
|
||||
import axios from '../utils/axios'
|
||||
import React, { createContext, useContext, useState, useEffect, type PropsWithChildren, useMemo } from 'react'
|
||||
import axios from 'utils/axios'
|
||||
import { type FormData, type Account } from '@types'
|
||||
|
||||
// init context
|
||||
const AuthContext = createContext()
|
||||
|
||||
// export the consumer
|
||||
export function useAuth() {
|
||||
return useContext(AuthContext)
|
||||
interface Context {
|
||||
token: string | null
|
||||
account: Account | null
|
||||
isLoggedIn: boolean
|
||||
register: (payload: FormData) => Promise<any>
|
||||
login: (payload: FormData) => Promise<any>
|
||||
logout: () => void
|
||||
}
|
||||
|
||||
// export the provider (handle all the logic here)
|
||||
export function AuthProvider({ children }) {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [account, setAccount] = useState(null)
|
||||
const [token, setToken] = useState(localStorage.getItem('token') || null)
|
||||
const initContext: Context = {
|
||||
token: null,
|
||||
account: null,
|
||||
isLoggedIn: false,
|
||||
register: async () => {},
|
||||
login: async () => {},
|
||||
logout: () => {},
|
||||
}
|
||||
|
||||
const register = (formData = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
// 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,
|
||||
},
|
||||
}) => {
|
||||
.then(({ data: { data: accountData, token: accessToken } }) => {
|
||||
setAccount(accountData)
|
||||
setToken(accessToken)
|
||||
setIsLoggedIn(true)
|
||||
resolve(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
reject(error?.response?.data?.message || error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const login = (formData = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const login = (formData: FormData) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.post('/auth/login', formData)
|
||||
.then(({
|
||||
data: {
|
||||
data: accountData,
|
||||
token: accessToken,
|
||||
},
|
||||
}) => {
|
||||
.then(({ data: { data: accountData, token: accessToken } }) => {
|
||||
setAccount(accountData)
|
||||
setToken(accessToken)
|
||||
setIsLoggedIn(true)
|
||||
resolve(true)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error)
|
||||
reject(error?.response?.data?.message || error.message)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const logout = () => {
|
||||
setIsLoggedIn(false)
|
||||
@@ -66,10 +74,7 @@ export function AuthProvider({ children }) {
|
||||
const loginWithToken = async () => {
|
||||
try {
|
||||
const {
|
||||
data: {
|
||||
data: accountData,
|
||||
token: accessToken,
|
||||
},
|
||||
data: { data: accountData, token: accessToken },
|
||||
} = await axios.get('/auth/login', {
|
||||
headers: {
|
||||
authorization: `Bearer ${token}`,
|
||||
@@ -79,7 +84,7 @@ export function AuthProvider({ children }) {
|
||||
setAccount(accountData)
|
||||
setToken(accessToken)
|
||||
setIsLoggedIn(true)
|
||||
} catch (error) {
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
if (error?.response?.statusCode === 401) setToken(null)
|
||||
}
|
||||
@@ -102,17 +107,7 @@ export function AuthProvider({ children }) {
|
||||
if (!isLoggedIn && !account && token) loginWithToken()
|
||||
}, [isLoggedIn, account, token]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<AuthContext.Provider
|
||||
value={{
|
||||
isLoggedIn,
|
||||
account,
|
||||
token,
|
||||
register,
|
||||
login,
|
||||
logout,
|
||||
}}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
const value = useMemo(() => ({ token, account, isLoggedIn, register, login, logout }), [token, account, isLoggedIn])
|
||||
|
||||
return <Provider value={value}>{children}</Provider>
|
||||
}
|
||||
19
client/src/functions/get-query-payload.ts
Normal file
19
client/src/functions/get-query-payload.ts
Normal 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
|
||||
@@ -1,19 +0,0 @@
|
||||
export default function getQuery(queryStr = '') {
|
||||
if (queryStr) {
|
||||
const queryObj = {}
|
||||
const queryArr = (
|
||||
queryStr[0] === '?' ? queryStr.substring(1, queryStr.length) : queryStr
|
||||
).split('&')
|
||||
|
||||
queryArr.forEach((str) => {
|
||||
const [key, val] = str.split('=')
|
||||
|
||||
queryObj[key] = val
|
||||
})
|
||||
|
||||
return queryObj
|
||||
} else {
|
||||
console.warn('Query string is not defined')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
export default function getTokenPayload(token = '') {
|
||||
if (token) {
|
||||
const informativePart = token.split('.')[1]
|
||||
const payload = JSON.parse(window.atob(informativePart))
|
||||
|
||||
return payload
|
||||
} else {
|
||||
console.warn('Token is not defined')
|
||||
return {}
|
||||
}
|
||||
}
|
||||
13
client/src/functions/get-token-payload.ts
Normal file
13
client/src/functions/get-token-payload.ts
Normal 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
|
||||
@@ -1,20 +0,0 @@
|
||||
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 ();
|
||||
}; */
|
||||
|
||||
export default function useLocalStorage(key = '', defaultValue = null) {
|
||||
const [value, setValue] = useState(JSON.parse(localStorage.getItem(key)) || defaultValue)
|
||||
|
||||
useEffect(() => {
|
||||
if (value != null) localStorage.setItem(key, JSON.stringify(value))
|
||||
}, [key, value])
|
||||
|
||||
return [value, setValue]
|
||||
}
|
||||
42
client/src/hooks/useLocalStorage.ts
Normal file
42
client/src/hooks/useLocalStorage.ts
Normal 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
|
||||
@@ -1,15 +1,19 @@
|
||||
import {useState, useEffect} from 'react'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
/* How to use this hook:
|
||||
/*
|
||||
|
||||
How to use this hook:
|
||||
import useMediaQuery from './hooks/useMediaQuery';
|
||||
|
||||
function App() {
|
||||
const isMobile = useMediaQuery('(max-width: 768px)');
|
||||
|
||||
return ();
|
||||
}; */
|
||||
};
|
||||
|
||||
export default function useMediaQuery(query = '(max-width: 768px)') {
|
||||
*/
|
||||
|
||||
const useMediaQuery = (query: string = '(max-width: 768px)') => {
|
||||
const [matches, setMatches] = useState(window.matchMedia(query).matches)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -22,3 +26,5 @@ export default function useMediaQuery(query = '(max-width: 768px)') {
|
||||
|
||||
return matches
|
||||
}
|
||||
|
||||
export default useMediaQuery
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import App from './App'
|
||||
import {AuthProvider} from './contexts/AuthContext'
|
||||
import CssBaseline from '@mui/material/CssBaseline'
|
||||
import '@fontsource/roboto'
|
||||
import './styles/index.css'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<CssBaseline />
|
||||
<App />
|
||||
</AuthProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root'),
|
||||
)
|
||||
18
client/src/index.tsx
Normal file
18
client/src/index.tsx
Normal 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
1
client/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
11
client/src/store/useModalStore.ts
Normal file
11
client/src/store/useModalStore.ts
Normal 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 }),
|
||||
}))
|
||||
23
client/src/styles/ReactWelcome.css
Normal file
23
client/src/styles/ReactWelcome.css
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -4,8 +4,16 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Roboto';
|
||||
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 {
|
||||
@@ -35,7 +43,7 @@ body {
|
||||
}
|
||||
|
||||
/*
|
||||
Extra Small Devices, Phones
|
||||
Extra Small Devices, Phones
|
||||
@media only screen and (min-width: 480px) {}
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import axios from 'axios'
|
||||
import { BACKEND_URL } from '../constants'
|
||||
|
||||
// axios configuration
|
||||
export default axios.create({
|
||||
baseURL: BACKEND_URL,
|
||||
})
|
||||
8
client/src/utils/axios.ts
Normal file
8
client/src/utils/axios.ts
Normal 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
27
client/tsconfig.json
Normal 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"]
|
||||
}
|
||||
539
package-lock.json
generated
539
package-lock.json
generated
@@ -1,539 +0,0 @@
|
||||
{
|
||||
"name": "mern-application",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "mern-application",
|
||||
"version": "0.1.0",
|
||||
"hasInstallScript": true,
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.3.0.tgz",
|
||||
"integrity": "sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.16.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^6.6.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^16.2.0"
|
||||
},
|
||||
"bin": {
|
||||
"concurrently": "bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
|
||||
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tslib": "^1.9.0"
|
||||
},
|
||||
"engines": {
|
||||
"npm": ">=2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"cliui": {
|
||||
"version": "7.0.4",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
|
||||
"integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.0",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"concurrently": {
|
||||
"version": "6.3.0",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-6.3.0.tgz",
|
||||
"integrity": "sha512-k4k1jQGHHKsfbqzkUszVf29qECBrkvBKkcPJEUDTyVR7tZd1G/JOfnst4g1sYbFvJ4UjHZisj1aWQR8yLKpGPw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chalk": "^4.1.0",
|
||||
"date-fns": "^2.16.1",
|
||||
"lodash": "^4.17.21",
|
||||
"rxjs": "^6.6.3",
|
||||
"spawn-command": "^0.0.2-1",
|
||||
"supports-color": "^8.1.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"yargs": "^16.2.0"
|
||||
}
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.24.0.tgz",
|
||||
"integrity": "sha512-6ujwvwgPID6zbI0o7UbURi2vlLDR9uP26+tW6Lg+Ji3w7dd0i3DOcjcClLjLPranT60SSEFBwdSyYwn/ZkPIuw==",
|
||||
"dev": true
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true
|
||||
},
|
||||
"escalade": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
|
||||
"integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
|
||||
"dev": true
|
||||
},
|
||||
"get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=",
|
||||
"dev": true
|
||||
},
|
||||
"rxjs": {
|
||||
"version": "6.6.7",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz",
|
||||
"integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"tslib": "^1.9.0"
|
||||
}
|
||||
},
|
||||
"spawn-command": {
|
||||
"version": "0.0.2-1",
|
||||
"resolved": "https://registry.npmjs.org/spawn-command/-/spawn-command-0.0.2-1.tgz",
|
||||
"integrity": "sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=",
|
||||
"dev": true
|
||||
},
|
||||
"string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
}
|
||||
},
|
||||
"strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true
|
||||
},
|
||||
"tslib": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz",
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
}
|
||||
},
|
||||
"y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true
|
||||
},
|
||||
"yargs": {
|
||||
"version": "16.2.0",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
|
||||
"integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"cliui": "^7.0.2",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.0",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^20.2.2"
|
||||
}
|
||||
},
|
||||
"yargs-parser": {
|
||||
"version": "20.2.9",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
|
||||
"integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
}
|
||||
21
package.json
21
package.json
@@ -1,27 +1,16 @@
|
||||
{
|
||||
"homepage": "https://github.com/belferink1996/MERN-template#readme",
|
||||
"name": "mern-application",
|
||||
"version": "0.1.0",
|
||||
"description": "",
|
||||
"private": true,
|
||||
"main": "",
|
||||
"license": "ISC",
|
||||
"keywords": [],
|
||||
"author": "Ben Elferink <ben.elferink@icloud.com> (https://www.linkedin.com/in/ben-elferink-37ba251b9)",
|
||||
"homepage": "/",
|
||||
"author": "Ben Elferink <ben.elferink@icloud.com> (https://github.com/BenElferink)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/belferink1996/MERN-template.git"
|
||||
"url": "https://github.com/BenElferink/mern-template.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/belferink1996/MERN-template/issues",
|
||||
"url": "https://github.com/BenElferink/mern-template/issues",
|
||||
"email": "ben.elferink@icloud.com"
|
||||
},
|
||||
"scripts": {
|
||||
"install": "concurrently \"cd ./server && npm i\" \"cd ./client && npm i\"",
|
||||
"start": "concurrently \"cd ./server && npm start\" \"cd ./client && npm start\"",
|
||||
"delete-modules": "rm -r ./node_modules && cd ./server && rm -r ./node_modules && cd ../client && rm -r ./node_modules"
|
||||
},
|
||||
"devDependencies": {
|
||||
"concurrently": "^6.3.0"
|
||||
}
|
||||
"scripts": {}
|
||||
}
|
||||
|
||||
21
server/.gitignore
vendored
21
server/.gitignore
vendored
@@ -1,21 +0,0 @@
|
||||
# dependencies
|
||||
/node_modules
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.eslintcache
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
@@ -1,25 +0,0 @@
|
||||
const Account = require('../../models/Account')
|
||||
const { signToken } = require('../../middlewares/jsonwebtoken')
|
||||
|
||||
async function loginWithToken(request, response, next) {
|
||||
try {
|
||||
const { uid } = request.auth
|
||||
|
||||
// Get account from DB, existance not verified because we are already authorized at this point
|
||||
const foundAccount = await Account.findOne({ _id: uid }).select('-password')
|
||||
|
||||
// Generate access token
|
||||
const token = signToken({ uid: foundAccount._id, role: foundAccount.role })
|
||||
|
||||
response.status(200).json({
|
||||
message: 'Account fetched',
|
||||
data: foundAccount,
|
||||
token,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
response.status(500).send()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = loginWithToken
|
||||
@@ -1,59 +0,0 @@
|
||||
const joi = require('joi')
|
||||
const bcrypt = require('bcrypt')
|
||||
const Account = require('../../models/Account')
|
||||
const {signToken} = require('../../middlewares/jsonwebtoken')
|
||||
|
||||
async function login(request, response, next) {
|
||||
try {
|
||||
// Validate request data
|
||||
await joi
|
||||
.object({
|
||||
username: joi.string().required(),
|
||||
password: joi.string().required(),
|
||||
})
|
||||
.validateAsync(request.body)
|
||||
} catch (error) {
|
||||
return response.status(400).json({
|
||||
error: 'ValidationError',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const {username, password} = request.body
|
||||
|
||||
// Get account from DB, and verify existance
|
||||
const foundAccount = await Account.findOne({username})
|
||||
if (!foundAccount) {
|
||||
return response.status(400).json({
|
||||
message: 'Bad credentials',
|
||||
})
|
||||
}
|
||||
|
||||
// Decrypt and verify password
|
||||
const passOk = await bcrypt.compare(password, foundAccount.password)
|
||||
if (!passOk) {
|
||||
return response.status(400).json({
|
||||
message: 'Bad credentials',
|
||||
})
|
||||
}
|
||||
|
||||
// Remove password from response data
|
||||
foundAccount.password = undefined
|
||||
delete foundAccount.password
|
||||
|
||||
// Generate access token
|
||||
const token = signToken({uid: foundAccount._id, role: foundAccount.role})
|
||||
|
||||
response.status(200).json({
|
||||
message: 'Succesfully logged-in',
|
||||
data: foundAccount,
|
||||
token,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
response.status(500).send()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = login
|
||||
@@ -1,60 +0,0 @@
|
||||
const joi = require('joi')
|
||||
const bcrypt = require('bcrypt')
|
||||
const Account = require('../../models/Account')
|
||||
const {signToken} = require('../../middlewares/jsonwebtoken')
|
||||
|
||||
async function register(request, response, next) {
|
||||
try {
|
||||
// Validate request data
|
||||
await joi
|
||||
.object({
|
||||
username: joi.string().required(),
|
||||
password: joi.string().required(),
|
||||
})
|
||||
.validateAsync(request.body)
|
||||
} catch (error) {
|
||||
return response.status(400).json({
|
||||
error: 'ValidationError',
|
||||
message: error.message,
|
||||
})
|
||||
}
|
||||
|
||||
try {
|
||||
const {username, password} = request.body
|
||||
|
||||
// Verify account username as unique
|
||||
const existingAccount = await Account.findOne({username})
|
||||
if (existingAccount) {
|
||||
return response.status(400).json({
|
||||
error: username,
|
||||
message: 'An account already exists with that "username"',
|
||||
})
|
||||
}
|
||||
|
||||
// Encrypt password
|
||||
const salt = await bcrypt.genSalt(10)
|
||||
const hash = await bcrypt.hash(password, salt)
|
||||
|
||||
// Create account
|
||||
const newAccount = new Account({username, password: hash})
|
||||
await newAccount.save()
|
||||
|
||||
// Remove password from response data
|
||||
newAccount.password = undefined
|
||||
delete newAccount.password
|
||||
|
||||
// Generate access token
|
||||
const token = signToken({uid: newAccount._id, role: newAccount.role})
|
||||
|
||||
response.status(201).json({
|
||||
message: 'Succesfully registered',
|
||||
data: newAccount,
|
||||
token,
|
||||
})
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return response.status(500).send()
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = register
|
||||
@@ -1,19 +0,0 @@
|
||||
require('dotenv').config() // Secures variables
|
||||
const app = require('./utils/app') // Backend App (server)
|
||||
const mongo = require('./utils/mongo') // MongoDB (database)
|
||||
const {PORT} = require('./constants')
|
||||
const authRoutes = require('./routes/auth')
|
||||
|
||||
async function bootstrap() {
|
||||
await mongo.connect()
|
||||
|
||||
app.get('/', (req, res) => res.status(200).json({message: 'Hello World!'}))
|
||||
app.get('/healthz', (req, res) => res.status(200).send())
|
||||
app.use('/auth', authRoutes)
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ Server is listening on port: ${PORT}`)
|
||||
})
|
||||
}
|
||||
|
||||
bootstrap()
|
||||
@@ -1,39 +0,0 @@
|
||||
const jwt = require('jsonwebtoken')
|
||||
const {JWT_SECRET} = require('../constants')
|
||||
|
||||
const signToken = (payload = {}, expiresIn = '12h') => {
|
||||
const token = jwt.sign(payload, JWT_SECRET, {expiresIn})
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
const authorizeBearerToken = (request, response, next) => {
|
||||
try {
|
||||
const token = request.headers.authorization?.split(' ')[1]
|
||||
if (!token) {
|
||||
return response.status(400).json({
|
||||
message: 'Token not provided',
|
||||
})
|
||||
}
|
||||
|
||||
const auth = jwt.verify(token, JWT_SECRET)
|
||||
if (!auth) {
|
||||
return response.status(401).json({
|
||||
message: 'Unauthorized - invalid token',
|
||||
})
|
||||
}
|
||||
|
||||
request.auth = auth
|
||||
next()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
return response.status(401).json({
|
||||
message: 'Unauthorized - invalid token',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authorizeBearerToken,
|
||||
signToken,
|
||||
}
|
||||
5627
server/package-lock.json
generated
5627
server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,21 +2,28 @@
|
||||
"name": "server",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"main": "index.js",
|
||||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"main": "src/index.ts",
|
||||
"scripts": {
|
||||
"start": "nodemon index.js || node index.js"
|
||||
"dev": "npx tsx src/index.ts",
|
||||
"build": "rm -rf dist && npx tsc",
|
||||
"start": "node dist/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"bcrypt": "^5.0.1",
|
||||
"bcrypt": "^5.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^10.0.0",
|
||||
"express": "^4.17.1",
|
||||
"joi": "^17.4.2",
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"mongoose": "^6.0.12"
|
||||
"dotenv": "^16.4.7",
|
||||
"express": "^4.21.2",
|
||||
"joi": "^17.13.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mongoose": "^8.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^2.0.14"
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/node": "^22.10.2",
|
||||
"typescript": "^5.7.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
const express = require('express')
|
||||
const { authorizeBearerToken } = require('../middlewares/jsonwebtoken')
|
||||
const register = require('../controllers/auth/register')
|
||||
const login = require('../controllers/auth/login')
|
||||
const loginWithToken = require('../controllers/auth/login-with-token')
|
||||
|
||||
// initialize router
|
||||
const router = express.Router()
|
||||
|
||||
// POST at route: http://localhost:8080/auth/register
|
||||
router.post('/register', [], register)
|
||||
|
||||
// POST at path: http://localhost:8080/auth/login
|
||||
router.post('/login', [], login)
|
||||
|
||||
// GET at path: http://localhost:8080/auth/account
|
||||
router.get('/login', [authorizeBearerToken], loginWithToken)
|
||||
|
||||
module.exports = router
|
||||
10
server/src/@types/express.d.ts
vendored
Normal file
10
server/src/@types/express.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
import 'express'
|
||||
import jwt from 'jsonwebtoken'
|
||||
|
||||
declare global {
|
||||
namespace Express {
|
||||
interface Request {
|
||||
auth?: jwt.JwtPayload // { uid: string; role: string }
|
||||
}
|
||||
}
|
||||
}
|
||||
5
server/src/@types/index.ts
Normal file
5
server/src/@types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export interface Account {
|
||||
username: string
|
||||
password: string
|
||||
role: 'user' | 'admin'
|
||||
}
|
||||
@@ -1,16 +1,11 @@
|
||||
const ORIGIN = '*'
|
||||
const PORT = process.env.PORT || 8080
|
||||
|
||||
// for "atlas" edit MONGO_URI in -> .env file || for "community server" edit <MyDatabase>
|
||||
const MONGO_URI = process.env.MONGO_URI || 'mongodb://localhost:27017/MyDatabase'
|
||||
// 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'
|
||||
|
||||
module.exports = {
|
||||
ORIGIN,
|
||||
PORT,
|
||||
MONGO_URI,
|
||||
MONGO_OPTIONS,
|
||||
JWT_SECRET,
|
||||
}
|
||||
export { ORIGIN, PORT, MONGO_URI, MONGO_OPTIONS, JWT_SECRET }
|
||||
32
server/src/controllers/auth/login-with-token.ts
Normal file
32
server/src/controllers/auth/login-with-token.ts
Normal 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
|
||||
59
server/src/controllers/auth/login.ts
Normal file
59
server/src/controllers/auth/login.ts
Normal 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
|
||||
56
server/src/controllers/auth/register.ts
Normal file
56
server/src/controllers/auth/register.ts
Normal 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
28
server/src/index.ts
Normal 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()
|
||||
35
server/src/middlewares/check-bearer-token.ts
Normal file
35
server/src/middlewares/check-bearer-token.ts
Normal 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
|
||||
12
server/src/middlewares/error-handler.ts
Normal file
12
server/src/middlewares/error-handler.ts
Normal 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
|
||||
@@ -1,9 +1,12 @@
|
||||
const mongoose = require('mongoose')
|
||||
import { type Document, model, Schema } from 'mongoose'
|
||||
import { type Account } from '../@types'
|
||||
|
||||
const instance = new mongoose.Schema(
|
||||
interface I extends Document, Account {}
|
||||
|
||||
const instance = new Schema<I>(
|
||||
{
|
||||
/*
|
||||
document ID is set by default via MongoDB - next line is deprecated
|
||||
document ID is set by default via MongoDB - the next line is deprecated!
|
||||
_id: mongoose.Schema.Types.ObjectId,
|
||||
*/
|
||||
|
||||
@@ -26,11 +29,11 @@ const instance = new mongoose.Schema(
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// NOTE! use a singular model name, mongoose automatically creates a collection like so:
|
||||
// model: 'Account' === collection: 'accounts'
|
||||
const modelName = 'Account'
|
||||
|
||||
module.exports = mongoose.model(modelName, instance)
|
||||
export default model<I>(modelName, instance)
|
||||
20
server/src/routes/auth.ts
Normal file
20
server/src/routes/auth.ts
Normal 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
13
server/src/utils/app.ts
Normal 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
22
server/src/utils/crypt.ts
Normal 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
22
server/src/utils/joi.ts
Normal 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
25
server/src/utils/jwt.ts
Normal 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()
|
||||
@@ -1,20 +1,25 @@
|
||||
const mongoose = require('mongoose')
|
||||
const {MONGO_URI} = require('../constants')
|
||||
const {MONGO_OPTIONS} = require('../constants')
|
||||
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
|
||||
|
||||
class MongoDB {
|
||||
constructor() {
|
||||
this.mongoose = mongoose
|
||||
this.mongoUri = MONGO_URI
|
||||
this.mongoOptions = MONGO_OPTIONS
|
||||
this.isConnected = false
|
||||
this.MONGO_URI = MONGO_URI
|
||||
this.MONGO_OPTIONS = MONGO_OPTIONS
|
||||
}
|
||||
|
||||
async connect() {
|
||||
if (this.isConnected) return
|
||||
|
||||
try {
|
||||
const db = await this.mongoose.connect(this.MONGO_URI, this.MONGO_OPTIONS)
|
||||
console.log('⏳ Connecting to MongoDB')
|
||||
|
||||
const db = await this.instance.connect(this.mongoUri, this.mongoOptions)
|
||||
const connection = db.connection
|
||||
|
||||
this.isConnected = connection.readyState === 1
|
||||
@@ -23,10 +28,10 @@ class MongoDB {
|
||||
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) {
|
||||
} catch (error: any) {
|
||||
console.log('❌ MongoDB connection error:', error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new MongoDB()
|
||||
export default new Mongo()
|
||||
15
server/tsconfig.json
Normal file
15
server/tsconfig.json
Normal 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"]
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
const express = require('express') // Backend App (server)
|
||||
const cors = require('cors') // HTTP headers (enable requests)
|
||||
const {ORIGIN} = require('../constants')
|
||||
|
||||
// initialize app
|
||||
const app = express()
|
||||
|
||||
// middlewares
|
||||
app.use(cors({origin: ORIGIN}))
|
||||
app.use(express.json({extended: true})) // body parser
|
||||
app.use(express.urlencoded({extended: false})) // url parser
|
||||
|
||||
// error handling
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err)
|
||||
res.status(500).send()
|
||||
next()
|
||||
})
|
||||
|
||||
module.exports = app
|
||||
Reference in New Issue
Block a user