mirror of
https://github.com/pawelmalak/snippet-box.git
synced 2025-12-21 13:23:05 +01:00
Login and register functionality
This commit is contained in:
@@ -1,25 +1,33 @@
|
||||
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 { Editor, Home, Snippet, Snippets, Auth } from './containers';
|
||||
import { AuthContextProvider, SnippetsContextProvider } from './store';
|
||||
import { ProtectedRoute } from './utils';
|
||||
import { AuthContext } from './store';
|
||||
|
||||
export const App = () => {
|
||||
const { autoLogin } = useContext(AuthContext);
|
||||
|
||||
useEffect(() => {
|
||||
// autoLogin();
|
||||
// const checker = setInterval(() => {
|
||||
// autoLogin();
|
||||
// console.log('cake');
|
||||
// }, 1000);
|
||||
// return () => window.clearInterval(checker);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<AuthContextProvider>
|
||||
<SnippetsContextProvider>
|
||||
<Navbar />
|
||||
<Switch>
|
||||
<Route exact path='/' component={Home} />
|
||||
<Route path='/snippets' component={Snippets} />
|
||||
<Route path='/snippet/:id' component={Snippet} />
|
||||
<Route path='/snippet/:id' component={Snippet} />
|
||||
<Route path='/auth' component={Auth} />
|
||||
<ProtectedRoute path='/editor/:id?' component={Editor} />
|
||||
</Switch>
|
||||
</SnippetsContextProvider>
|
||||
</AuthContextProvider>
|
||||
</BrowserRouter>
|
||||
<Fragment>
|
||||
<Navbar />
|
||||
<Switch>
|
||||
<Route exact path='/' component={Home} />
|
||||
<Route path='/snippets' component={Snippets} />
|
||||
<Route path='/snippet/:id' component={Snippet} />
|
||||
<Route path='/snippet/:id' component={Snippet} />
|
||||
<Route path='/auth' component={Auth} />
|
||||
<ProtectedRoute path='/editor/:id?' component={Editor} />
|
||||
</Switch>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button } from '../UI';
|
||||
export const AuthForm = (): JSX.Element => {
|
||||
const [isInLogin, setIsInLogin] = useState(true);
|
||||
const [formData, setFormData] = useState({ email: '', password: '' });
|
||||
const { login } = useContext(AuthContext);
|
||||
const { login, register } = useContext(AuthContext);
|
||||
|
||||
const inputHandler = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
@@ -21,7 +21,7 @@ export const AuthForm = (): JSX.Element => {
|
||||
if (isInLogin) {
|
||||
login(formData);
|
||||
} else {
|
||||
// register
|
||||
register(formData);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Card, Layout } from '../components/UI';
|
||||
export const Auth = (): JSX.Element => {
|
||||
return (
|
||||
<Layout>
|
||||
<div className='col-12 col-md-6 mx-auto'>
|
||||
<div className='col-12 col-md-8 mx-auto'>
|
||||
<Card>
|
||||
<AuthForm />
|
||||
</Card>
|
||||
|
||||
@@ -1,11 +1,20 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './styles/style.scss';
|
||||
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { AuthContextProvider, SnippetsContextProvider } from './store';
|
||||
import { App } from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<BrowserRouter>
|
||||
<AuthContextProvider>
|
||||
<SnippetsContextProvider>
|
||||
<App />
|
||||
</SnippetsContextProvider>
|
||||
</AuthContextProvider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
2
client/src/store/auth/actions/index.ts
Normal file
2
client/src/store/auth/actions/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './registerUser';
|
||||
export * from './loginUser';
|
||||
39
client/src/store/auth/actions/loginUser.ts
Normal file
39
client/src/store/auth/actions/loginUser.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
37
client/src/store/auth/actions/registerUser.ts
Normal file
37
client/src/store/auth/actions/registerUser.ts
Normal 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);
|
||||
}
|
||||
};
|
||||
71
client/src/store/auth/index.tsx
Normal file
71
client/src/store/auth/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1,2 @@
|
||||
export * from './SnippetsContext';
|
||||
export * from './AuthContext';
|
||||
export * from './auth';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TagCount, NewSnippet, Snippet, SearchQuery } from '.';
|
||||
import { TagCount, NewSnippet, Snippet, SearchQuery, UserWithRole } from '.';
|
||||
|
||||
export interface SnippetsContext {
|
||||
snippets: Snippet[];
|
||||
@@ -18,5 +18,8 @@ export interface SnippetsContext {
|
||||
|
||||
export interface AuthContext {
|
||||
isAuthenticated: boolean;
|
||||
user: UserWithRole | null;
|
||||
autoLogin: () => void;
|
||||
login: (formData: { email: string; password: string }) => void;
|
||||
register: (formData: { email: string; password: string }) => void;
|
||||
}
|
||||
|
||||
@@ -2,4 +2,9 @@ import { Model } from '.';
|
||||
|
||||
export interface User extends Model {
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export interface UserWithRole extends Model {
|
||||
isAdmin: boolean;
|
||||
}
|
||||
|
||||
51
src/controllers/auth/createUser.ts
Normal file
51
src/controllers/auth/createUser.ts
Normal 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
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
3
src/controllers/auth/index.ts
Normal file
3
src/controllers/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './createUser';
|
||||
export * from './loginUser';
|
||||
export * from './getProfile';
|
||||
60
src/controllers/auth/loginUser.ts
Normal file
60
src/controllers/auth/loginUser.ts
Normal 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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
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 =
|
||||
(foo: Foo) => (req: Request, res: Response, next: NextFunction) => {
|
||||
|
||||
46
src/middleware/authenticate.ts
Normal file
46
src/middleware/authenticate.ts
Normal 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'));
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './asyncWrapper';
|
||||
export * from './requireBody';
|
||||
export * from './errorHandler';
|
||||
export * from './authenticate';
|
||||
|
||||
13
src/routes/auth.ts
Normal file
13
src/routes/auth.ts
Normal 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
2
src/routes/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './snippets';
|
||||
export * from './auth';
|
||||
@@ -6,7 +6,7 @@ import { connectDB } from './db';
|
||||
import { errorHandler } from './middleware';
|
||||
|
||||
// Routers
|
||||
import { snippetRouter } from './routes/snippets';
|
||||
import { snippetRouter, authRouter } from './routes';
|
||||
import { associateModels } from './db/associateModels';
|
||||
|
||||
// Env config
|
||||
@@ -27,6 +27,7 @@ app.get(/^\/(?!api)/, (req: Request, res: Response) => {
|
||||
|
||||
// Routes
|
||||
app.use('/api/snippets', snippetRouter);
|
||||
app.use('/api/auth', authRouter);
|
||||
|
||||
// Error handler
|
||||
app.use(errorHandler);
|
||||
|
||||
10
src/typescript/interfaces/Request.ts
Normal file
10
src/typescript/interfaces/Request.ts
Normal 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;
|
||||
};
|
||||
}
|
||||
3
src/typescript/interfaces/Token.ts
Normal file
3
src/typescript/interfaces/Token.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface Token {
|
||||
email: string;
|
||||
}
|
||||
@@ -5,3 +5,5 @@ export * from './Snippet_Tag';
|
||||
export * from './Body';
|
||||
export * from './SearchQuery';
|
||||
export * from './User';
|
||||
export * from './Token';
|
||||
export * from './Request';
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { sign } from 'jsonwebtoken';
|
||||
import { Token } from '../typescript/interfaces';
|
||||
|
||||
type Data = string | object | Buffer;
|
||||
|
||||
export const signToken = (data: Data): string => {
|
||||
const secret =
|
||||
process.env.JWT_SECRET || 'x7-joXEF89Q5hUx9Od5mibNVQb9vUuLr1091TMZSM-w';
|
||||
export const signToken = (data: Token): string => {
|
||||
const secret = process.env.JWT_SECRET || 'secret';
|
||||
|
||||
const token = sign(data, secret, {
|
||||
expiresIn: '30d'
|
||||
|
||||
Reference in New Issue
Block a user