Login and register functionality

This commit is contained in:
Paweł Malak
2021-10-20 13:02:12 +02:00
parent 63bed57c28
commit 8499c46bbb
25 changed files with 394 additions and 73 deletions

View File

@@ -1,14 +1,24 @@
import { BrowserRouter, Switch, Route } from 'react-router-dom'; import { Fragment, useContext, useEffect } from 'react';
import { Switch, Route } from 'react-router-dom';
import { Navbar } from './components/Navigation/Navbar'; import { Navbar } from './components/Navigation/Navbar';
import { Editor, Home, Snippet, Snippets, Auth } from './containers'; import { Editor, Home, Snippet, Snippets, Auth } from './containers';
import { AuthContextProvider, SnippetsContextProvider } from './store';
import { ProtectedRoute } from './utils'; import { ProtectedRoute } from './utils';
import { AuthContext } from './store';
export const App = () => { export const App = () => {
const { autoLogin } = useContext(AuthContext);
useEffect(() => {
// autoLogin();
// const checker = setInterval(() => {
// autoLogin();
// console.log('cake');
// }, 1000);
// return () => window.clearInterval(checker);
}, []);
return ( return (
<BrowserRouter> <Fragment>
<AuthContextProvider>
<SnippetsContextProvider>
<Navbar /> <Navbar />
<Switch> <Switch>
<Route exact path='/' component={Home} /> <Route exact path='/' component={Home} />
@@ -18,8 +28,6 @@ export const App = () => {
<Route path='/auth' component={Auth} /> <Route path='/auth' component={Auth} />
<ProtectedRoute path='/editor/:id?' component={Editor} /> <ProtectedRoute path='/editor/:id?' component={Editor} />
</Switch> </Switch>
</SnippetsContextProvider> </Fragment>
</AuthContextProvider>
</BrowserRouter>
); );
}; };

View File

@@ -5,7 +5,7 @@ import { Button } from '../UI';
export const AuthForm = (): JSX.Element => { export const AuthForm = (): JSX.Element => {
const [isInLogin, setIsInLogin] = useState(true); const [isInLogin, setIsInLogin] = useState(true);
const [formData, setFormData] = useState({ email: '', password: '' }); const [formData, setFormData] = useState({ email: '', password: '' });
const { login } = useContext(AuthContext); const { login, register } = useContext(AuthContext);
const inputHandler = (e: ChangeEvent<HTMLInputElement>) => { const inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target; const { name, value } = e.target;
@@ -21,7 +21,7 @@ export const AuthForm = (): JSX.Element => {
if (isInLogin) { if (isInLogin) {
login(formData); login(formData);
} else { } else {
// register register(formData);
} }
}; };

View File

@@ -4,7 +4,7 @@ import { Card, Layout } from '../components/UI';
export const Auth = (): JSX.Element => { export const Auth = (): JSX.Element => {
return ( return (
<Layout> <Layout>
<div className='col-12 col-md-6 mx-auto'> <div className='col-12 col-md-8 mx-auto'>
<Card> <Card>
<AuthForm /> <AuthForm />
</Card> </Card>

View File

@@ -1,11 +1,20 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import './styles/style.scss'; import './styles/style.scss';
import { BrowserRouter } from 'react-router-dom';
import { AuthContextProvider, SnippetsContextProvider } from './store';
import { App } from './App'; import { App } from './App';
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter>
<AuthContextProvider>
<SnippetsContextProvider>
<App /> <App />
</SnippetsContextProvider>
</AuthContextProvider>
</BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
document.getElementById('root') document.getElementById('root')
); );

View File

@@ -1,43 +0,0 @@
import axios from 'axios';
import { createContext, ReactNode } from 'react';
import { AuthContext as Context, Response } from '../typescript/interfaces';
import { errorHandler } from '../utils';
export const AuthContext = createContext<Context>({
isAuthenticated: false,
login: () => {}
});
interface Props {
children: ReactNode;
}
export const AuthContextProvider = (props: Props): JSX.Element => {
const login = async (formData: { email: string; password: string }) => {
try {
const res = await axios.post<Response<{ token: string }>>(
'/api/auth/login',
formData
);
localStorage.setItem('token', res.data.data.token);
// get profile
// redirect to snippets? / home?
} catch (err) {
errorHandler(err);
}
};
const context: Context = {
isAuthenticated: false,
login
};
return (
<AuthContext.Provider value={context}>
{props.children}
</AuthContext.Provider>
);
};

View File

@@ -0,0 +1,2 @@
export * from './registerUser';
export * from './loginUser';

View File

@@ -0,0 +1,39 @@
import { User, Response, UserWithRole } from '../../../typescript/interfaces';
import { errorHandler } from '../../../utils';
import axios from 'axios';
import React from 'react';
interface Params {
formData: {
email: string;
password: string;
};
setIsAuthenticated: (v: React.SetStateAction<boolean>) => void;
setUser: (v: React.SetStateAction<UserWithRole | null>) => void;
}
export const loginUser = async (params: Params) => {
const { formData, setIsAuthenticated, setUser } = params;
try {
const res = await axios.post<Response<{ token: string; user: User }>>(
'/api/auth/login',
formData
);
const { token: resToken, user: resUser } = res.data.data;
setUser({
...resUser,
isAdmin: resUser.role === 'admin'
});
localStorage.setItem('token', resToken);
setIsAuthenticated(true);
// redirect to snippets? / home?
} catch (err) {
errorHandler(err);
}
};

View File

@@ -0,0 +1,37 @@
import { User, Response, UserWithRole } from '../../../typescript/interfaces';
import { errorHandler } from '../../../utils';
import axios from 'axios';
import React from 'react';
interface Params {
formData: {
email: string;
password: string;
};
setIsAuthenticated: (v: React.SetStateAction<boolean>) => void;
setUser: (v: React.SetStateAction<UserWithRole | null>) => void;
}
export const registerUser = async (params: Params) => {
const { formData, setIsAuthenticated, setUser } = params;
try {
const res = await axios.post<Response<{ token: string; user: User }>>(
'/api/auth/register',
formData
);
const { token: resToken, user: resUser } = res.data.data;
setUser({
...resUser,
isAdmin: resUser.role === 'admin'
});
localStorage.setItem('token', resToken);
setIsAuthenticated(true);
} catch (err) {
errorHandler(err);
}
};

View File

@@ -0,0 +1,71 @@
import { createContext, ReactNode, useState } from 'react';
import axios from 'axios';
import { loginUser, registerUser } from './actions';
import {
AuthContext as Context,
Response,
User,
UserWithRole
} from '../../typescript/interfaces';
import { errorHandler } from '../../utils';
export const AuthContext = createContext<Context>({
isAuthenticated: false,
user: null,
autoLogin: () => {},
login: () => {},
register: () => {}
});
interface Props {
children: ReactNode;
}
export const AuthContextProvider = (props: Props): JSX.Element => {
const [user, setUser] = useState<UserWithRole | null>(null);
const [isAuthenticated, setIsAuthenticated] = useState(false);
const autoLogin = async () => {
if (localStorage.token) {
await getProfile(localStorage.token);
}
};
const login = async (formData: { email: string; password: string }) => {
await loginUser({ formData, setIsAuthenticated, setUser });
};
const register = async (formData: { email: string; password: string }) => {
await registerUser({ formData, setIsAuthenticated, setUser });
};
const getProfile = async (token: string) => {
try {
const res = await axios.get<Response<User>>('/api/auth/me', {
headers: {
Authorization: `Bearer ${token}`
}
});
console.log(res.data.data);
} catch (err) {
errorHandler(err);
}
};
const context: Context = {
isAuthenticated,
user,
autoLogin,
login,
register
};
return (
<AuthContext.Provider value={context}>
{props.children}
</AuthContext.Provider>
);
};

View File

@@ -1,2 +1,2 @@
export * from './SnippetsContext'; export * from './SnippetsContext';
export * from './AuthContext'; export * from './auth';

View File

@@ -1,4 +1,4 @@
import { TagCount, NewSnippet, Snippet, SearchQuery } from '.'; import { TagCount, NewSnippet, Snippet, SearchQuery, UserWithRole } from '.';
export interface SnippetsContext { export interface SnippetsContext {
snippets: Snippet[]; snippets: Snippet[];
@@ -18,5 +18,8 @@ export interface SnippetsContext {
export interface AuthContext { export interface AuthContext {
isAuthenticated: boolean; isAuthenticated: boolean;
user: UserWithRole | null;
autoLogin: () => void;
login: (formData: { email: string; password: string }) => void; login: (formData: { email: string; password: string }) => void;
register: (formData: { email: string; password: string }) => void;
} }

View File

@@ -2,4 +2,9 @@ import { Model } from '.';
export interface User extends Model { export interface User extends Model {
email: string; email: string;
role: string;
}
export interface UserWithRole extends Model {
isAdmin: boolean;
} }

View File

@@ -0,0 +1,51 @@
import { Request, Response, NextFunction } from 'express';
import { asyncWrapper } from '../../middleware';
import { UserInstance, UserModel } from '../../models';
import { hashPassword, signToken } from '../../utils';
interface RequestBody {
email: string;
password: string;
}
interface ResponseBody {
data: {
user: Omit<UserInstance, 'password'>;
token: string;
};
}
/**
* @description Create new user
* @route /api/auth
* @request POST
* @access Public
*/
export const createUser = asyncWrapper(
async (
req: Request<{}, {}, RequestBody>,
res: Response<ResponseBody>,
next: NextFunction
): Promise<void> => {
const password = await hashPassword(req.body.password);
await UserModel.create({
...req.body,
password
});
const user = (await UserModel.findOne({
where: { email: req.body.email },
attributes: { exclude: ['password'] }
})) as UserInstance;
const token = signToken({ email: req.body.email });
res.status(201).json({
data: {
user,
token
}
});
}
);

View File

@@ -0,0 +1,3 @@
export * from './createUser';
export * from './loginUser';
export * from './getProfile';

View File

@@ -0,0 +1,60 @@
import { Request, Response, NextFunction } from 'express';
import { asyncWrapper } from '../../middleware';
import { UserInstance, UserModel } from '../../models';
import { ErrorResponse, signToken } from '../../utils';
import { compare } from 'bcrypt';
interface RequestBody {
email: string;
password: string;
}
interface ResponseBody {
data: {
user: Omit<UserInstance, 'password'>;
token: string;
};
}
/**
* @description User login
* @route /api/auth/login
* @request POST
* @access Public
*/
export const loginUser = asyncWrapper(
async (
req: Request<{}, {}, RequestBody>,
res: Response<ResponseBody>,
next: NextFunction
): Promise<void> => {
const { email, password } = req.body;
// Find user
let user = await UserModel.findOne({ where: { email }, raw: true });
if (user) {
// Check password
const passwordMatch = await compare(password, user.password);
if (passwordMatch) {
// Sign token
const token = await signToken({ email });
user = (await UserModel.findOne({
where: { email },
attributes: { exclude: ['password'] },
raw: true
})) as UserInstance;
res.status(200).json({
data: { token, user }
});
} else {
return next(new ErrorResponse(400, 'Invalid credentials'));
}
} else {
return next(new ErrorResponse(400, 'Invalid credentials'));
}
}
);

View File

@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
type Foo = (req: Request, res: Response, next: NextFunction) => Promise<void>; type Foo = (req: any, res: Response, next: NextFunction) => Promise<void>;
export const asyncWrapper = export const asyncWrapper =
(foo: Foo) => (req: Request, res: Response, next: NextFunction) => { (foo: Foo) => (req: Request, res: Response, next: NextFunction) => {

View File

@@ -0,0 +1,46 @@
import { NextFunction, Request, Response } from 'express';
import { asyncWrapper } from '.';
import { ErrorResponse } from '../utils';
import { verify } from 'jsonwebtoken';
import { Token, UserInfoRequest } from '../typescript/interfaces';
import { UserModel } from '../models';
export const authenticate = asyncWrapper(
async (req: UserInfoRequest, res: Response, next: NextFunction) => {
let token: string | null = null;
// Check if token was provided
if (req.headers.authorization) {
if (req.headers.authorization.startsWith('Bearer ')) {
token = req.headers.authorization.split(' ')[1];
}
}
if (token) {
const secret = process.env.JWT_SECRET || 'secret';
// Decode token and extract data
const decoded = verify(token, secret) as Token;
// Find user
const user = await UserModel.findOne({
where: { email: decoded.email },
attributes: { exclude: ['password'] },
raw: true
});
if (user) {
req.user = {
...user,
isAdmin: user.role == 'admin'
};
next();
} else {
return next(new ErrorResponse(401, 'Not authorized'));
}
} else {
return next(new ErrorResponse(401, 'Not authorized'));
}
}
);

View File

@@ -1,3 +1,4 @@
export * from './asyncWrapper'; export * from './asyncWrapper';
export * from './requireBody'; export * from './requireBody';
export * from './errorHandler'; export * from './errorHandler';
export * from './authenticate';

13
src/routes/auth.ts Normal file
View File

@@ -0,0 +1,13 @@
import { Router } from 'express';
import { createUser, getProfile, loginUser } from '../controllers/auth';
import { authenticate, requireBody } from '../middleware';
export const authRouter = Router();
authRouter
.route('/register')
.post(requireBody('email', 'password'), createUser);
authRouter.route('/login').post(requireBody('email', 'password'), loginUser);
authRouter.route('/me').get(authenticate, getProfile);

2
src/routes/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './snippets';
export * from './auth';

View File

@@ -6,7 +6,7 @@ import { connectDB } from './db';
import { errorHandler } from './middleware'; import { errorHandler } from './middleware';
// Routers // Routers
import { snippetRouter } from './routes/snippets'; import { snippetRouter, authRouter } from './routes';
import { associateModels } from './db/associateModels'; import { associateModels } from './db/associateModels';
// Env config // Env config
@@ -27,6 +27,7 @@ app.get(/^\/(?!api)/, (req: Request, res: Response) => {
// Routes // Routes
app.use('/api/snippets', snippetRouter); app.use('/api/snippets', snippetRouter);
app.use('/api/auth', authRouter);
// Error handler // Error handler
app.use(errorHandler); app.use(errorHandler);

View File

@@ -0,0 +1,10 @@
import { Request } from 'express';
export interface UserInfoRequest<body = {}> extends Request<{}, {}, body> {
user: {
id: number;
email: string;
role: string;
isAdmin: boolean;
};
}

View File

@@ -0,0 +1,3 @@
export interface Token {
email: string;
}

View File

@@ -5,3 +5,5 @@ export * from './Snippet_Tag';
export * from './Body'; export * from './Body';
export * from './SearchQuery'; export * from './SearchQuery';
export * from './User'; export * from './User';
export * from './Token';
export * from './Request';

View File

@@ -1,10 +1,8 @@
import { sign } from 'jsonwebtoken'; import { sign } from 'jsonwebtoken';
import { Token } from '../typescript/interfaces';
type Data = string | object | Buffer; export const signToken = (data: Token): string => {
const secret = process.env.JWT_SECRET || 'secret';
export const signToken = (data: Data): string => {
const secret =
process.env.JWT_SECRET || 'x7-joXEF89Q5hUx9Od5mibNVQb9vUuLr1091TMZSM-w';
const token = sign(data, secret, { const token = sign(data, secret, {
expiresIn: '30d' expiresIn: '30d'