diff --git a/client/src/index.tsx b/client/src/index.tsx
index 8458eb3..8e97138 100644
--- a/client/src/index.tsx
+++ b/client/src/index.tsx
@@ -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(
-
+
+
+
+
+
+
+
,
document.getElementById('root')
);
diff --git a/client/src/store/AuthContext.tsx b/client/src/store/AuthContext.tsx
deleted file mode 100644
index 04ca979..0000000
--- a/client/src/store/AuthContext.tsx
+++ /dev/null
@@ -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
({
- 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>(
- '/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 (
-
- {props.children}
-
- );
-};
diff --git a/client/src/store/auth/actions/index.ts b/client/src/store/auth/actions/index.ts
new file mode 100644
index 0000000..f3d5fd7
--- /dev/null
+++ b/client/src/store/auth/actions/index.ts
@@ -0,0 +1,2 @@
+export * from './registerUser';
+export * from './loginUser';
diff --git a/client/src/store/auth/actions/loginUser.ts b/client/src/store/auth/actions/loginUser.ts
new file mode 100644
index 0000000..2bd10c1
--- /dev/null
+++ b/client/src/store/auth/actions/loginUser.ts
@@ -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) => void;
+ setUser: (v: React.SetStateAction) => void;
+}
+
+export const loginUser = async (params: Params) => {
+ const { formData, setIsAuthenticated, setUser } = params;
+
+ try {
+ const res = await axios.post>(
+ '/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);
+ }
+};
diff --git a/client/src/store/auth/actions/registerUser.ts b/client/src/store/auth/actions/registerUser.ts
new file mode 100644
index 0000000..deedbc9
--- /dev/null
+++ b/client/src/store/auth/actions/registerUser.ts
@@ -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) => void;
+ setUser: (v: React.SetStateAction) => void;
+}
+
+export const registerUser = async (params: Params) => {
+ const { formData, setIsAuthenticated, setUser } = params;
+
+ try {
+ const res = await axios.post>(
+ '/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);
+ }
+};
diff --git a/client/src/store/auth/index.tsx b/client/src/store/auth/index.tsx
new file mode 100644
index 0000000..38f1876
--- /dev/null
+++ b/client/src/store/auth/index.tsx
@@ -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({
+ isAuthenticated: false,
+ user: null,
+ autoLogin: () => {},
+ login: () => {},
+ register: () => {}
+});
+
+interface Props {
+ children: ReactNode;
+}
+
+export const AuthContextProvider = (props: Props): JSX.Element => {
+ const [user, setUser] = useState(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>('/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 (
+
+ {props.children}
+
+ );
+};
diff --git a/client/src/store/index.ts b/client/src/store/index.ts
index efc489c..8442e7b 100644
--- a/client/src/store/index.ts
+++ b/client/src/store/index.ts
@@ -1,2 +1,2 @@
export * from './SnippetsContext';
-export * from './AuthContext';
+export * from './auth';
diff --git a/client/src/typescript/interfaces/Context.ts b/client/src/typescript/interfaces/Context.ts
index 531ddb7..dac9dff 100644
--- a/client/src/typescript/interfaces/Context.ts
+++ b/client/src/typescript/interfaces/Context.ts
@@ -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;
}
diff --git a/client/src/typescript/interfaces/User.ts b/client/src/typescript/interfaces/User.ts
index b6a1093..0c88b00 100644
--- a/client/src/typescript/interfaces/User.ts
+++ b/client/src/typescript/interfaces/User.ts
@@ -2,4 +2,9 @@ import { Model } from '.';
export interface User extends Model {
email: string;
+ role: string;
+}
+
+export interface UserWithRole extends Model {
+ isAdmin: boolean;
}
diff --git a/src/controllers/auth/createUser.ts b/src/controllers/auth/createUser.ts
new file mode 100644
index 0000000..27097dc
--- /dev/null
+++ b/src/controllers/auth/createUser.ts
@@ -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;
+ token: string;
+ };
+}
+
+/**
+ * @description Create new user
+ * @route /api/auth
+ * @request POST
+ * @access Public
+ */
+export const createUser = asyncWrapper(
+ async (
+ req: Request<{}, {}, RequestBody>,
+ res: Response,
+ next: NextFunction
+ ): Promise => {
+ 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
+ }
+ });
+ }
+);
diff --git a/src/controllers/auth/index.ts b/src/controllers/auth/index.ts
new file mode 100644
index 0000000..42b1066
--- /dev/null
+++ b/src/controllers/auth/index.ts
@@ -0,0 +1,3 @@
+export * from './createUser';
+export * from './loginUser';
+export * from './getProfile';
diff --git a/src/controllers/auth/loginUser.ts b/src/controllers/auth/loginUser.ts
new file mode 100644
index 0000000..6003a42
--- /dev/null
+++ b/src/controllers/auth/loginUser.ts
@@ -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;
+ token: string;
+ };
+}
+
+/**
+ * @description User login
+ * @route /api/auth/login
+ * @request POST
+ * @access Public
+ */
+export const loginUser = asyncWrapper(
+ async (
+ req: Request<{}, {}, RequestBody>,
+ res: Response,
+ next: NextFunction
+ ): Promise => {
+ 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'));
+ }
+ }
+);
diff --git a/src/middleware/asyncWrapper.ts b/src/middleware/asyncWrapper.ts
index ae23ab6..63be0ff 100644
--- a/src/middleware/asyncWrapper.ts
+++ b/src/middleware/asyncWrapper.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
-type Foo = (req: Request, res: Response, next: NextFunction) => Promise;
+type Foo = (req: any, res: Response, next: NextFunction) => Promise;
export const asyncWrapper =
(foo: Foo) => (req: Request, res: Response, next: NextFunction) => {
diff --git a/src/middleware/authenticate.ts b/src/middleware/authenticate.ts
new file mode 100644
index 0000000..4201d9c
--- /dev/null
+++ b/src/middleware/authenticate.ts
@@ -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'));
+ }
+ }
+);
diff --git a/src/middleware/index.ts b/src/middleware/index.ts
index ebcbe86..c116d25 100644
--- a/src/middleware/index.ts
+++ b/src/middleware/index.ts
@@ -1,3 +1,4 @@
export * from './asyncWrapper';
export * from './requireBody';
export * from './errorHandler';
+export * from './authenticate';
diff --git a/src/routes/auth.ts b/src/routes/auth.ts
new file mode 100644
index 0000000..21ea84e
--- /dev/null
+++ b/src/routes/auth.ts
@@ -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);
diff --git a/src/routes/index.ts b/src/routes/index.ts
new file mode 100644
index 0000000..4829c7c
--- /dev/null
+++ b/src/routes/index.ts
@@ -0,0 +1,2 @@
+export * from './snippets';
+export * from './auth';
diff --git a/src/server.ts b/src/server.ts
index dfc64ea..e764b00 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -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);
diff --git a/src/typescript/interfaces/Request.ts b/src/typescript/interfaces/Request.ts
new file mode 100644
index 0000000..374513f
--- /dev/null
+++ b/src/typescript/interfaces/Request.ts
@@ -0,0 +1,10 @@
+import { Request } from 'express';
+
+export interface UserInfoRequest extends Request<{}, {}, body> {
+ user: {
+ id: number;
+ email: string;
+ role: string;
+ isAdmin: boolean;
+ };
+}
diff --git a/src/typescript/interfaces/Token.ts b/src/typescript/interfaces/Token.ts
new file mode 100644
index 0000000..fac1aed
--- /dev/null
+++ b/src/typescript/interfaces/Token.ts
@@ -0,0 +1,3 @@
+export interface Token {
+ email: string;
+}
diff --git a/src/typescript/interfaces/index.ts b/src/typescript/interfaces/index.ts
index 4f61f90..3ea2e45 100644
--- a/src/typescript/interfaces/index.ts
+++ b/src/typescript/interfaces/index.ts
@@ -5,3 +5,5 @@ export * from './Snippet_Tag';
export * from './Body';
export * from './SearchQuery';
export * from './User';
+export * from './Token';
+export * from './Request';
diff --git a/src/utils/signToken.ts b/src/utils/signToken.ts
index 09cc0b4..b01ef00 100644
--- a/src/utils/signToken.ts
+++ b/src/utils/signToken.ts
@@ -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'