new release with reactjs
This commit is contained in:
parent
c9595ccb61
commit
20aae2dc2d
14
README.md
14
README.md
@ -48,24 +48,20 @@ This project uses `eslint` and `prettier`. all configurations for this project i
|
|||||||
|
|
||||||
4- `GRANT ALL PRIVILEGES ON DATABASE authentication TO admin;`
|
4- `GRANT ALL PRIVILEGES ON DATABASE authentication TO admin;`
|
||||||
|
|
||||||
**[2]** Second to install the node_modules run `npm install` or `yarn install`. After installation is done
|
**[2]** Second to install the node_modules for the backend, navigate to the frontend directory and run `npm install` or `yarn install`. After installation is done
|
||||||
migrate up the database table schema with `npm run migrate:up` or `yarn migrate:up`
|
migrate up the database table schema with `npm run migrate:up` or `yarn migrate:up`
|
||||||
or run the `script.sh` in `sql` folder from root directory with `./script.sh`
|
or run the `script.sh` in `sql` folder from root directory with `./script.sh`
|
||||||
after that start the api in dev mode with `npm run dev` or `yarn dev`.
|
after that start the api in dev mode with `npm run dev` or `yarn dev`.
|
||||||
|
|
||||||
|
**[3]** Third, to install the node_modules for the frontend, navigate to the frontend directory and run `npm install` or `yarn install`. Once the installation is complete, start the frontend server in development mode by running `npm run dev` or `yarn dev`.
|
||||||
|
|
||||||
## Unit Tests
|
## Unit Tests
|
||||||
|
|
||||||
Unit test available using Jasmine with this command: `npm run test`
|
Unit test available using Jasmine with this command: `npm run test` or `yarn test`
|
||||||
|
|
||||||
## Important Note
|
|
||||||
|
|
||||||
```
|
|
||||||
To use refreshToken in the front end I provided a file called react.js in the documents folder.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Scripts
|
## Available Scripts
|
||||||
|
|
||||||
In the project directory, you can run:
|
In the project backend directory, you can run:
|
||||||
|
|
||||||
##### `npm run dev` or `yarn dev`
|
##### `npm run dev` or `yarn dev`
|
||||||
|
|
||||||
|
|||||||
@ -29,4 +29,4 @@ JWT_REFRESH_TOKEN_EXPIRATION=86400
|
|||||||
|
|
||||||
# URL
|
# URL
|
||||||
BACKEND_HOST=http://127.0.0.1:8000
|
BACKEND_HOST=http://127.0.0.1:8000
|
||||||
FRONTEND_HOST=http://127.0.0.1:3000
|
FRONTEND_HOST=http://127.0.0.1:5173
|
||||||
@ -43,7 +43,6 @@ CREATE TABLE IF NOT EXISTS users (
|
|||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
first_name VARCHAR(100) NOT NULL,
|
first_name VARCHAR(100) NOT NULL,
|
||||||
last_name VARCHAR(100) NOT NULL,
|
last_name VARCHAR(100) NOT NULL,
|
||||||
username VARCHAR(100) UNIQUE NOT NULL,
|
|
||||||
email VARCHAR(255) UNIQUE NOT NULL,
|
email VARCHAR(255) UNIQUE NOT NULL,
|
||||||
password TEXT NOT NULL,
|
password TEXT NOT NULL,
|
||||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { createUser } from './register';
|
import { createUser } from './register';
|
||||||
import { authUser } from './login';
|
import { authUser } from './login';
|
||||||
|
import { authMe } from '../../utils/token';
|
||||||
import { updatePassword } from './updatePassword';
|
import { updatePassword } from './updatePassword';
|
||||||
import { refreshAccessToken } from './refreshAccessToken';
|
import { refreshAccessToken } from './refreshAccessToken';
|
||||||
|
|
||||||
export { createUser, authUser, updatePassword, refreshAccessToken };
|
export { createUser, authUser, authMe, updatePassword, refreshAccessToken };
|
||||||
@ -28,14 +28,27 @@ export const authUser = async (req: Request, res: Response) => {
|
|||||||
maxAge: configs.refresh_expires
|
maxAge: configs.refresh_expires
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Note:
|
||||||
|
If you are not using frontend server like React, Angular.
|
||||||
|
it's better to remove the refreshToken from the response body
|
||||||
|
*/
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
status: true,
|
status: true,
|
||||||
data: { user: { ...response }, accessToken },
|
data: { user: { ...response }, accessToken, refreshToken },
|
||||||
message: 'User authenticated successfully.'
|
message: 'User authenticated successfully.'
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
if ((error as Error).message.includes('Password')) {
|
||||||
message: (error as Error).message
|
return res
|
||||||
});
|
.status(400)
|
||||||
|
.json({ errors: [{ password: (error as Error).message }] });
|
||||||
|
}
|
||||||
|
if ((error as Error).message.includes('Email')) {
|
||||||
|
return res
|
||||||
|
.status(400)
|
||||||
|
.json({ errors: [{ email: (error as Error).message }] });
|
||||||
|
}
|
||||||
|
res.status(400).json({ errors: (error as Error).message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -17,19 +17,17 @@ export const refreshAccessToken = async (req: Request, res: Response) => {
|
|||||||
// Verify the refresh token
|
// Verify the refresh token
|
||||||
const decoded = await verifyRefreshToken(token);
|
const decoded = await verifyRefreshToken(token);
|
||||||
// Generate a new access and refresh tokens
|
// Generate a new access and refresh tokens
|
||||||
const { id, first_name, last_name, username, email } = decoded;
|
const { id, first_name, last_name, email } = decoded;
|
||||||
const accessToken = await setAccessToken({
|
const accessToken = await setAccessToken({
|
||||||
id,
|
id,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
const refreshToken = await setRefreshToken({
|
const refreshToken = await setRefreshToken({
|
||||||
id,
|
id,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -51,7 +49,7 @@ export const refreshAccessToken = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
message: (error as Error).message
|
error: (error as Error).message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -13,7 +13,7 @@ export const deleteUser = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
message: (error as Error).message
|
error: (error as Error).message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -13,7 +13,7 @@ export const getUser = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
message: (error as Error).message
|
error: (error as Error).message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -13,7 +13,7 @@ export const updateUser = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
message: (error as Error).message
|
error: (error as Error).message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
23
backend/src/middlewares/validation/auth/validateLogin.ts
Normal file
23
backend/src/middlewares/validation/auth/validateLogin.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { body, oneOf } from 'express-validator';
|
||||||
|
import { validate } from '../validationResult';
|
||||||
|
|
||||||
|
export const validateLogin = [
|
||||||
|
body('email')
|
||||||
|
.exists()
|
||||||
|
.withMessage('Email is missing from the body')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('Email is empty')
|
||||||
|
.isEmail()
|
||||||
|
.withMessage('Email is not valid')
|
||||||
|
.normalizeEmail()
|
||||||
|
.withMessage('Email is not normalized'),
|
||||||
|
body('password')
|
||||||
|
.exists()
|
||||||
|
.withMessage('password is missing from the body')
|
||||||
|
.notEmpty()
|
||||||
|
.withMessage('password is empty')
|
||||||
|
.isLength({ min: 8 })
|
||||||
|
.withMessage('password must be at least 8 characters long'),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
|
||||||
|
];
|
||||||
@ -5,27 +5,20 @@ import { validate } from '../validationResult';
|
|||||||
export const validateCreateUser = [
|
export const validateCreateUser = [
|
||||||
body('first_name')
|
body('first_name')
|
||||||
.exists()
|
.exists()
|
||||||
.withMessage("first_name does'nt exists in the body.")
|
.withMessage("First Name does'nt exists in the body.")
|
||||||
.notEmpty()
|
.notEmpty()
|
||||||
.withMessage('first_name is empty')
|
.withMessage('First Name is empty')
|
||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 5, max: 50 })
|
.isLength({ min: 5, max: 50 })
|
||||||
.withMessage('first_name must be at least 5 and maximum 50 letters'),
|
.withMessage('First Name must be at least 5 and maximum 50 letters'),
|
||||||
body('last_name')
|
body('last_name')
|
||||||
.exists()
|
.exists()
|
||||||
.withMessage("last_name does'nt exists in the body.")
|
.withMessage("Last Name does'nt exists in the body.")
|
||||||
.notEmpty()
|
.notEmpty()
|
||||||
.withMessage('last_name is empty')
|
.withMessage('Last Name is empty')
|
||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 5, max: 50 })
|
.isLength({ min: 5, max: 50 })
|
||||||
.withMessage('last_name must be at least 5 and maximum 50 letters'),
|
.withMessage('Last Name must be at least 5 and maximum 50 letters'),
|
||||||
body('username')
|
|
||||||
.exists()
|
|
||||||
.withMessage('username is missing from the body')
|
|
||||||
.notEmpty()
|
|
||||||
.withMessage('username is empty')
|
|
||||||
.isString()
|
|
||||||
.withMessage('username must be string'),
|
|
||||||
body('email')
|
body('email')
|
||||||
.exists()
|
.exists()
|
||||||
.withMessage('Email is missing from the body')
|
.withMessage('Email is missing from the body')
|
||||||
@ -37,10 +30,10 @@ export const validateCreateUser = [
|
|||||||
.withMessage('Email is not normalized'),
|
.withMessage('Email is not normalized'),
|
||||||
body('password')
|
body('password')
|
||||||
.exists()
|
.exists()
|
||||||
.withMessage('password is missing from the body')
|
.withMessage('Password is missing from the body')
|
||||||
.notEmpty()
|
.notEmpty()
|
||||||
.withMessage('password is empty')
|
.withMessage('Password is empty')
|
||||||
.isLength({ min: 8 })
|
.isLength({ min: 8 })
|
||||||
.withMessage('password must be at least 8 characters long'),
|
.withMessage('Password must be at least 8 characters long'),
|
||||||
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
|
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
|
||||||
];
|
];
|
||||||
@ -24,13 +24,6 @@ export const validateUpdateUser = [
|
|||||||
.isString()
|
.isString()
|
||||||
.isLength({ min: 5, max: 50 })
|
.isLength({ min: 5, max: 50 })
|
||||||
.withMessage('last_name must be at least 5 and maximum 50 letters'),
|
.withMessage('last_name must be at least 5 and maximum 50 letters'),
|
||||||
body('username')
|
|
||||||
.exists()
|
|
||||||
.withMessage('username is missing from the body')
|
|
||||||
.notEmpty()
|
|
||||||
.withMessage('username is empty')
|
|
||||||
.isString()
|
|
||||||
.withMessage('username must be string'),
|
|
||||||
body('email')
|
body('email')
|
||||||
.exists()
|
.exists()
|
||||||
.withMessage('Email is missing from the body')
|
.withMessage('Email is missing from the body')
|
||||||
@ -10,7 +10,7 @@ export const validate = async (
|
|||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
const errorArray = errors
|
const errorArray = errors
|
||||||
.array()
|
.array()
|
||||||
.map((error) => ({ [error.param]: [error.msg] }));
|
.map((error) => ({ [error.param]: error.msg }));
|
||||||
return res.status(422).json({ errors: errorArray });
|
return res.status(422).json({ errors: errorArray });
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
@ -37,7 +37,7 @@ class Auth {
|
|||||||
const check = await compare(u.password, hash);
|
const check = await compare(u.password, hash);
|
||||||
if (check) {
|
if (check) {
|
||||||
const query = {
|
const query = {
|
||||||
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE email=$1',
|
text: 'SELECT id, first_name, last_name, email FROM users WHERE email=$1',
|
||||||
values: [u.email]
|
values: [u.email]
|
||||||
};
|
};
|
||||||
const result = await connection.query(query);
|
const result = await connection.query(query);
|
||||||
@ -51,7 +51,7 @@ class Auth {
|
|||||||
async authMe(id: string): Promise<UserType & AuthType> {
|
async authMe(id: string): Promise<UserType & AuthType> {
|
||||||
return this.withConnection(async (connection: PoolClient) => {
|
return this.withConnection(async (connection: PoolClient) => {
|
||||||
const query = {
|
const query = {
|
||||||
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE id=$1',
|
text: 'SELECT id, first_name, last_name, email FROM users WHERE id=$1',
|
||||||
values: [id]
|
values: [id]
|
||||||
};
|
};
|
||||||
const result = await connection.query(query);
|
const result = await connection.query(query);
|
||||||
@ -18,7 +18,6 @@ describe('User Model', () => {
|
|||||||
const newUser1 = {
|
const newUser1 = {
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Haddad',
|
last_name: 'Haddad',
|
||||||
username: 'adhamhaddad',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com',
|
email: 'adhamhaddad.dev@gmail.com',
|
||||||
password: 'adham123'
|
password: 'adham123'
|
||||||
} as UserType;
|
} as UserType;
|
||||||
@ -26,7 +25,6 @@ describe('User Model', () => {
|
|||||||
const updateUser1 = {
|
const updateUser1 = {
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Ashraf',
|
last_name: 'Ashraf',
|
||||||
username: 'adhamhaddad1',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com'
|
email: 'adhamhaddad.dev@gmail.com'
|
||||||
} as UserType;
|
} as UserType;
|
||||||
|
|
||||||
@ -48,7 +46,6 @@ describe('User Model', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Haddad',
|
last_name: 'Haddad',
|
||||||
username: 'adhamhaddad',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com'
|
email: 'adhamhaddad.dev@gmail.com'
|
||||||
} as UserType);
|
} as UserType);
|
||||||
});
|
});
|
||||||
@ -59,7 +56,6 @@ describe('User Model', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Haddad',
|
last_name: 'Haddad',
|
||||||
username: 'adhamhaddad',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com'
|
email: 'adhamhaddad.dev@gmail.com'
|
||||||
} as UserType);
|
} as UserType);
|
||||||
});
|
});
|
||||||
@ -70,7 +66,6 @@ describe('User Model', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Ashraf',
|
last_name: 'Ashraf',
|
||||||
username: 'adhamhaddad1',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com'
|
email: 'adhamhaddad.dev@gmail.com'
|
||||||
} as UserType);
|
} as UserType);
|
||||||
});
|
});
|
||||||
@ -118,7 +113,6 @@ describe('Auth Model', () => {
|
|||||||
id: 1,
|
id: 1,
|
||||||
first_name: 'Adham',
|
first_name: 'Adham',
|
||||||
last_name: 'Ashraf',
|
last_name: 'Ashraf',
|
||||||
username: 'adhamhaddad1',
|
|
||||||
email: 'adhamhaddad.dev@gmail.com'
|
email: 'adhamhaddad.dev@gmail.com'
|
||||||
} as UserType);
|
} as UserType);
|
||||||
});
|
});
|
||||||
@ -3,10 +3,9 @@ import { pgClient } from '../database';
|
|||||||
import { hash } from '../utils/password';
|
import { hash } from '../utils/password';
|
||||||
|
|
||||||
export type UserType = {
|
export type UserType = {
|
||||||
id: number;
|
id: string;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -31,11 +30,11 @@ class User {
|
|||||||
const password = await hash(u.password);
|
const password = await hash(u.password);
|
||||||
const query = {
|
const query = {
|
||||||
text: `
|
text: `
|
||||||
INSERT INTO users (first_name, last_name, username, email, password)
|
INSERT INTO users (first_name, last_name, email, password)
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING id, first_name, last_name, username, email
|
RETURNING id, first_name, last_name, email
|
||||||
`,
|
`,
|
||||||
values: [u.first_name, u.last_name, u.username, u.email, password]
|
values: [u.first_name, u.last_name, u.email, password]
|
||||||
};
|
};
|
||||||
const result = await connection.query(query);
|
const result = await connection.query(query);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
@ -44,7 +43,7 @@ class User {
|
|||||||
async getUser(id: string): Promise<UserType> {
|
async getUser(id: string): Promise<UserType> {
|
||||||
return this.withConnection(async (connection: PoolClient) => {
|
return this.withConnection(async (connection: PoolClient) => {
|
||||||
const query = {
|
const query = {
|
||||||
text: 'SELECT id, first_name, last_name, username, email FROM users WHERE id=$1',
|
text: 'SELECT id, first_name, last_name, email FROM users WHERE id=$1',
|
||||||
values: [id]
|
values: [id]
|
||||||
};
|
};
|
||||||
const result = await connection.query(query);
|
const result = await connection.query(query);
|
||||||
@ -55,11 +54,11 @@ class User {
|
|||||||
return this.withConnection(async (connection: PoolClient) => {
|
return this.withConnection(async (connection: PoolClient) => {
|
||||||
const query = {
|
const query = {
|
||||||
text: `
|
text: `
|
||||||
UPDATE users SET first_name=$2, last_name=$3, username=$4, email=$5, updated_at=CURRENT_TIMESTAMP
|
UPDATE users SET first_name=$2, last_name=$3, email=$4, updated_at=CURRENT_TIMESTAMP
|
||||||
WHERE id=$1
|
WHERE id=$1
|
||||||
RETURNING id, first_name, last_name, username, email
|
RETURNING id, first_name, last_name, email
|
||||||
`,
|
`,
|
||||||
values: [id, u.first_name, u.last_name, u.username, u.email]
|
values: [id, u.first_name, u.last_name, u.email]
|
||||||
};
|
};
|
||||||
const result = await connection.query(query);
|
const result = await connection.query(query);
|
||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
@ -7,6 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
createUser,
|
createUser,
|
||||||
authUser,
|
authUser,
|
||||||
|
authMe,
|
||||||
refreshAccessToken,
|
refreshAccessToken,
|
||||||
updatePassword
|
updatePassword
|
||||||
} from '../../controllers/auth';
|
} from '../../controllers/auth';
|
||||||
@ -18,6 +19,7 @@ router
|
|||||||
.post('/register', validateRegister, createUser)
|
.post('/register', validateRegister, createUser)
|
||||||
.post('/login', validateLogin, authUser)
|
.post('/login', validateLogin, authUser)
|
||||||
.patch('/reset-password', validateUpdatePassword, verifyToken, updatePassword)
|
.patch('/reset-password', validateUpdatePassword, verifyToken, updatePassword)
|
||||||
.post('/refresh-token', refreshAccessToken);
|
.post('/refresh-token', refreshAccessToken)
|
||||||
|
.get('/auth-me', authMe);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@ -22,6 +22,7 @@ const corsOptions = {
|
|||||||
'Content-Type',
|
'Content-Type',
|
||||||
'Accept',
|
'Accept',
|
||||||
'X-Access-Token',
|
'X-Access-Token',
|
||||||
|
'X-Refresh-Token',
|
||||||
'Authorization',
|
'Authorization',
|
||||||
'Access-Control-Allow-Origin',
|
'Access-Control-Allow-Origin',
|
||||||
'Access-Control-Allow-Headers',
|
'Access-Control-Allow-Headers',
|
||||||
@ -29,7 +30,8 @@ const corsOptions = {
|
|||||||
],
|
],
|
||||||
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
|
methods: 'GET,HEAD,OPTIONS,PUT,PATCH,POST,DELETE',
|
||||||
preflightContinue: true,
|
preflightContinue: true,
|
||||||
origin: '*'
|
origin: true,
|
||||||
|
credentials: true
|
||||||
};
|
};
|
||||||
|
|
||||||
// Middlewares
|
// Middlewares
|
||||||
11
backend/src/utils/password.ts
Normal file
11
backend/src/utils/password.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import configs from '../configs';
|
||||||
|
|
||||||
|
export const hash = async (password: string) =>
|
||||||
|
await bcrypt.hash(
|
||||||
|
`${configs.pepper}${password}${configs.pepper}`,
|
||||||
|
configs.salt
|
||||||
|
);
|
||||||
|
|
||||||
|
export const compare = async (password: string, hash: string) =>
|
||||||
|
await bcrypt.compare(`${configs.pepper}${password}${configs.pepper}`, hash);
|
||||||
104
backend/src/utils/token/authMe.ts
Normal file
104
backend/src/utils/token/authMe.ts
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import { Request as ExpressRequest, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { redisClient } from '../../database';
|
||||||
|
import configs from '../../configs';
|
||||||
|
import { setAccessToken, verifyRefreshToken, DecodedToken, Payload } from '.';
|
||||||
|
|
||||||
|
const publicAccessKey = path.join(
|
||||||
|
__dirname,
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'..',
|
||||||
|
'keys',
|
||||||
|
'accessToken',
|
||||||
|
'public.key'
|
||||||
|
);
|
||||||
|
interface Request extends ExpressRequest {
|
||||||
|
user?: DecodedToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authMe = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const authorization = req.headers.authorization as string;
|
||||||
|
if (!authorization) {
|
||||||
|
return res.status(401).json({
|
||||||
|
message: 'Not Authorized'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const [bearer, token] = authorization.split(' ');
|
||||||
|
if (bearer !== 'Bearer' || !token) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid Authorization header format. Format is "Bearer <token>".'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const publicKey = await fs.promises.readFile(publicAccessKey, 'utf8');
|
||||||
|
const decoded = jwt.verify(token, publicKey, {
|
||||||
|
algorithms: ['RS256'],
|
||||||
|
issuer: 'Nodejs-Refresh-Token'
|
||||||
|
}) as DecodedToken;
|
||||||
|
const cachedToken = await redisClient.get(`access_token:${decoded.id}`);
|
||||||
|
if (!cachedToken || cachedToken !== token) {
|
||||||
|
throw new Error('Access token not found or expired');
|
||||||
|
}
|
||||||
|
const { id, first_name, last_name, email } = decoded;
|
||||||
|
|
||||||
|
req.user = { id: id };
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
data: {
|
||||||
|
user: { id, first_name, last_name, email },
|
||||||
|
accessToken: token
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if ((err as Error).name !== 'TokenExpiredError') {
|
||||||
|
throw new Error('Invalid access token');
|
||||||
|
}
|
||||||
|
// Get Refresh-Token
|
||||||
|
const refreshToken = req.get('X-Refresh-Token') as string;
|
||||||
|
|
||||||
|
if (!refreshToken) {
|
||||||
|
throw new Error('Refresh token missing');
|
||||||
|
}
|
||||||
|
const [bearer, token] = refreshToken.split(' ');
|
||||||
|
if (bearer !== 'Bearer' || !token) {
|
||||||
|
throw new Error(
|
||||||
|
'Invalid Authorization header format. Format is "Bearer <token>".'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const decoded = await verifyRefreshToken(token);
|
||||||
|
const { id, first_name, last_name, email } = decoded;
|
||||||
|
const newAccessToken = await setAccessToken({
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach user object to request and proceed with new access token
|
||||||
|
req.user = { id: String(decoded.id) };
|
||||||
|
res.cookie('accessToken', newAccessToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: false,
|
||||||
|
maxAge: configs.access_expires
|
||||||
|
});
|
||||||
|
return res.status(200).json({
|
||||||
|
data: {
|
||||||
|
user: {
|
||||||
|
id,
|
||||||
|
first_name,
|
||||||
|
last_name,
|
||||||
|
email
|
||||||
|
},
|
||||||
|
accessToken: newAccessToken
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
res.status(401).json({ message: (err as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -2,16 +2,19 @@ import { setAccessToken } from './setAccessToken';
|
|||||||
import { setRefreshToken } from './setRefreshToken';
|
import { setRefreshToken } from './setRefreshToken';
|
||||||
import { verifyAccessToken } from './verifyAccessToken';
|
import { verifyAccessToken } from './verifyAccessToken';
|
||||||
import { verifyRefreshToken } from './verifyRefreshToken';
|
import { verifyRefreshToken } from './verifyRefreshToken';
|
||||||
|
import { authMe } from './authMe';
|
||||||
|
|
||||||
interface Payload {
|
interface Payload {
|
||||||
id: number;
|
id: string;
|
||||||
first_name: string;
|
first_name: string;
|
||||||
last_name: string;
|
last_name: string;
|
||||||
username: string;
|
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
interface DecodedToken {
|
interface DecodedToken {
|
||||||
id: string;
|
id: string;
|
||||||
|
first_name?: string;
|
||||||
|
last_name?: string;
|
||||||
|
email?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -19,6 +22,7 @@ export {
|
|||||||
setRefreshToken,
|
setRefreshToken,
|
||||||
verifyAccessToken,
|
verifyAccessToken,
|
||||||
verifyRefreshToken,
|
verifyRefreshToken,
|
||||||
|
authMe,
|
||||||
Payload,
|
Payload,
|
||||||
DecodedToken
|
DecodedToken
|
||||||
};
|
};
|
||||||
@ -3,9 +3,8 @@ import jwt from 'jsonwebtoken';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { redisClient } from '../../database';
|
import { redisClient } from '../../database';
|
||||||
import { DecodedToken } from '.';
|
|
||||||
import { verifyRefreshToken, setAccessToken } from '.';
|
|
||||||
import configs from '../../configs';
|
import configs from '../../configs';
|
||||||
|
import { verifyRefreshToken, setAccessToken, DecodedToken } from '.';
|
||||||
|
|
||||||
const publicAccessKey = path.join(
|
const publicAccessKey = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
@ -49,29 +48,35 @@ export const verifyAccessToken = async (
|
|||||||
throw new Error('Access token not found or expired');
|
throw new Error('Access token not found or expired');
|
||||||
}
|
}
|
||||||
req.user = { id: decoded.id };
|
req.user = { id: decoded.id };
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if ((err as Error).name !== 'TokenExpiredError') {
|
if ((err as Error).name !== 'TokenExpiredError') {
|
||||||
throw new Error('Invalid access token');
|
throw new Error('Invalid access token');
|
||||||
}
|
}
|
||||||
|
// Get Refresh-Token
|
||||||
|
const refreshToken = req.get('X-Refresh-Token') as string;
|
||||||
|
|
||||||
const refreshToken = req.cookies.refreshToken;
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('Refresh token missing');
|
throw new Error('Refresh token missing');
|
||||||
}
|
}
|
||||||
|
const [bearer, token] = refreshToken.split(' ');
|
||||||
const decoded = await verifyRefreshToken(refreshToken);
|
if (bearer !== 'Bearer' || !token) {
|
||||||
const { id, first_name, last_name, username, email } = decoded;
|
throw new Error(
|
||||||
|
'Invalid Authorization header format. Format is "Bearer <token>".'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const decoded = await verifyRefreshToken(token);
|
||||||
|
const { id, first_name, last_name, email } = decoded;
|
||||||
const newAccessToken = await setAccessToken({
|
const newAccessToken = await setAccessToken({
|
||||||
id,
|
id,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
username,
|
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
// Attach user object to request and proceed with new access token
|
// Attach user object to request and proceed with new access token
|
||||||
req.user = { id: String(decoded.id) };
|
req.user = { id };
|
||||||
res.cookie('accessToken', newAccessToken, {
|
res.cookie('accessToken', newAccessToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
@ -12,7 +12,6 @@ The request body should be a JSON object with the following properties:
|
|||||||
{
|
{
|
||||||
"first_name": "Adham",
|
"first_name": "Adham",
|
||||||
"last_name": "Haddad",
|
"last_name": "Haddad",
|
||||||
"username": "adhamhaddad",
|
|
||||||
"email": "adhamhaddad.dev@gmail.com",
|
"email": "adhamhaddad.dev@gmail.com",
|
||||||
"password": "secret-password"
|
"password": "secret-password"
|
||||||
}
|
}
|
||||||
@ -27,11 +26,12 @@ If the user is successfully created, the server will respond with a status code
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"first_name": "Adham",
|
"first_name": "Adham",
|
||||||
"last_name": "Haddad",
|
"last_name": "Haddad",
|
||||||
"username": "adhamhaddad",
|
|
||||||
"email": "adhamhaddad.dev@gmail.com"
|
"email": "adhamhaddad.dev@gmail.com"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
## POST /auth/login
|
## POST /auth/login
|
||||||
|
|
||||||
Authenticate user.
|
Authenticate user.
|
||||||
@ -57,23 +57,28 @@ If the user is exists and authenticated successfully, the server will respond wi
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"first_name": "Adham",
|
"first_name": "Adham",
|
||||||
"last_name": "Haddad",
|
"last_name": "Haddad",
|
||||||
"username": "adhamhaddad",
|
"email": "adhamhaddad.dev@gmail.com"
|
||||||
"email": "adhamhaddad.dev@gmail.com",
|
|
||||||
},
|
},
|
||||||
"accessToken": "<Access-Token>"
|
"accessToken": "<Access-Token>",
|
||||||
|
"refreshToken": "<Refresh-Token>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## POST /auth/refresh-token
|
<hr />
|
||||||
|
|
||||||
|
## POST /auth/auth-me
|
||||||
|
|
||||||
Refresh the access and refresh tokens.
|
Refresh the access and refresh tokens.
|
||||||
|
|
||||||
### Request Headers
|
### Request Headers
|
||||||
|
|
||||||
The request headers should have a Cookies with the following properties:
|
The request headers should have the following properties:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
refreshToken="<Refresh-Token>"
|
"headers": {
|
||||||
|
"Authorization": "Bearer <Access-Token>",
|
||||||
|
"X-Refresh-Token": "Bearer <Refresh-Token>"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
@ -82,21 +87,52 @@ If the refresh token is exists in redis and valid, the server will respond with
|
|||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"accessToken": "<Access-Token>",
|
"accessToken": "<Access-Token>"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
## POST /auth/refresh-token
|
||||||
|
|
||||||
|
Refresh the access and refresh tokens.
|
||||||
|
|
||||||
|
### Request Headers
|
||||||
|
|
||||||
|
The request headers should have the following properties:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"headers": {
|
||||||
|
"Authorization": "Bearer <Access-Token>",
|
||||||
|
"X-Refresh-Token": "Bearer <Refresh-Token>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Response
|
||||||
|
|
||||||
|
If the refresh token is exists in redis and valid, the server will respond with a status code of 200 and a JSON object representing a new tokens:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"accessToken": "<Access-Token>"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
## GET /users/:userId
|
## GET /users/:userId
|
||||||
|
|
||||||
Get a user by id.
|
Get a user by id.
|
||||||
|
|
||||||
### Request Headers
|
### Request Headers
|
||||||
|
|
||||||
The request headers should have a Cookies with the following properties:
|
The request headers should have the following properties:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
accessToken="<Access-Token>"
|
"headers": {
|
||||||
refreshToken="<Refresh-Token>"
|
"Authorization": "Bearer <Access-Token>",
|
||||||
|
"X-Refresh-Token": "Bearer <Refresh-Token>"
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Response
|
### Response
|
||||||
@ -108,47 +144,6 @@ If the user is exists, the server will respond with a status code of 200 and a J
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"first_name": "Adham",
|
"first_name": "Adham",
|
||||||
"last_name": "Haddad",
|
"last_name": "Haddad",
|
||||||
"username": "adhamhaddad",
|
|
||||||
"email": "adhamhaddad.dev@gmail.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## PATCH /users/:userId
|
|
||||||
|
|
||||||
Get a user by id.
|
|
||||||
|
|
||||||
### Request Headers
|
|
||||||
|
|
||||||
The request headers should have a Cookies with the following properties:
|
|
||||||
|
|
||||||
```json
|
|
||||||
accessToken="<Access-Token>"
|
|
||||||
refreshToken="<Refresh-Token>"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Request Body
|
|
||||||
|
|
||||||
The request body should be a JSON object with the following properties:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"first_name": "Adham",
|
|
||||||
"last_name": "Ashraf",
|
|
||||||
"username": "adhamhaddad",
|
|
||||||
"email": "adhamhaddad.dev@gmail.com"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Response
|
|
||||||
|
|
||||||
If the user is exists and updated, the server will respond with a status code of 204 and a JSON object representing the received user:
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"id": 1,
|
|
||||||
"first_name": "Adham",
|
|
||||||
"last_name": "Ashraf",
|
|
||||||
"username": "adhamhaddad",
|
|
||||||
"email": "adhamhaddad.dev@gmail.com"
|
"email": "adhamhaddad.dev@gmail.com"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
67
documents/react.js
vendored
67
documents/react.js
vendored
@ -1,67 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
|
|
||||||
const Login = () => {
|
|
||||||
const [accessToken, setAccessToken] = useState(null);
|
|
||||||
const [refreshToken, setRefreshToken] = useState(null);
|
|
||||||
const [values, setValues] = useState({
|
|
||||||
email: '',
|
|
||||||
password: ''
|
|
||||||
});
|
|
||||||
const handleChange = (prop) => (event) => {
|
|
||||||
setValues((prev) => ({ ...prev, [prop]: event.target.value }));
|
|
||||||
};
|
|
||||||
const handleSubmit = async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const response = await fetch('/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ email, password })
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
// The login was successful, extract the access token from the response
|
|
||||||
const { accessToken } = data;
|
|
||||||
|
|
||||||
// Retrieve the refresh token from the HTTP-only cookie
|
|
||||||
const cookies = document.cookie.split('; ');
|
|
||||||
const refreshTokenCookie = cookies.find((cookie) =>
|
|
||||||
cookie.startsWith('refreshToken=')
|
|
||||||
);
|
|
||||||
const refreshToken = refreshTokenCookie
|
|
||||||
? refreshTokenCookie.split('=')[1]
|
|
||||||
: null;
|
|
||||||
|
|
||||||
// Store the access token and refresh token in state
|
|
||||||
setAccessToken(accessToken);
|
|
||||||
setRefreshToken(refreshToken);
|
|
||||||
} else {
|
|
||||||
// The login failed, display the error message
|
|
||||||
setError(data.message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<input
|
|
||||||
type='email'
|
|
||||||
name='email'
|
|
||||||
value={values.email}
|
|
||||||
onChange={handleChange('email')}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type='password'
|
|
||||||
name='password'
|
|
||||||
value={values.password}
|
|
||||||
onChange={handleChange('password')}
|
|
||||||
/>
|
|
||||||
<button type='submit'>Login</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Login;
|
|
||||||
15
frontend/.eslintrc.cjs
Normal file
15
frontend/.eslintrc.cjs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
module.exports = {
|
||||||
|
env: { browser: true, es2020: true },
|
||||||
|
extends: [
|
||||||
|
'eslint:recommended',
|
||||||
|
'plugin:react/recommended',
|
||||||
|
'plugin:react/jsx-runtime',
|
||||||
|
'plugin:react-hooks/recommended',
|
||||||
|
],
|
||||||
|
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
||||||
|
settings: { react: { version: '18.2' } },
|
||||||
|
plugins: ['react-refresh'],
|
||||||
|
rules: {
|
||||||
|
'react-refresh/only-export-components': 'warn',
|
||||||
|
},
|
||||||
|
}
|
||||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
frontend/package.json
Normal file
29
frontend/package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "eslint src --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"axios": "^1.4.0",
|
||||||
|
"js-cookie": "^3.0.5",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.0.28",
|
||||||
|
"@types/react-dom": "^18.0.11",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"eslint": "^8.38.0",
|
||||||
|
"eslint-plugin-react": "^7.32.2",
|
||||||
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
|
"vite": "^4.3.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
frontend/public/vite.svg
Normal file
1
frontend/public/vite.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
33
frontend/src/App.jsx
Normal file
33
frontend/src/App.jsx
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Switch, Route, Redirect } from 'react-router-dom';
|
||||||
|
import { useAuth } from '@hooks';
|
||||||
|
import RegisterPage from '@pages/register';
|
||||||
|
import LoginPage from '@pages/login';
|
||||||
|
import HomePage from '@pages/home';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const { isLogged, user } = useAuth();
|
||||||
|
return user ? (
|
||||||
|
<>
|
||||||
|
<Switch>
|
||||||
|
<Route path='/login' exact>
|
||||||
|
<Redirect to='/' />
|
||||||
|
</Route>
|
||||||
|
<Route path='/register' exact>
|
||||||
|
<Redirect to='/' />
|
||||||
|
</Route>
|
||||||
|
<Route path='/' exact>
|
||||||
|
<HomePage />
|
||||||
|
</Route>
|
||||||
|
</Switch>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Route exact path='/login' component={LoginPage} />
|
||||||
|
<Route exact path='/register' component={RegisterPage} />
|
||||||
|
<Route exact path='*' render={() => <Redirect exact to='/login' />} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
34
frontend/src/components/UI/input/index.jsx
Normal file
34
frontend/src/components/UI/input/index.jsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import styles from '@styles/input.module.css';
|
||||||
|
|
||||||
|
const Input = ({
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
type,
|
||||||
|
placeholder,
|
||||||
|
value,
|
||||||
|
style,
|
||||||
|
isValid,
|
||||||
|
onChange,
|
||||||
|
onBlur
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div style={style} className={styles['input-box']}>
|
||||||
|
<label htmlFor={id} className={styles['input-box_label']}>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
className={`${styles['input-box_input']} ${
|
||||||
|
isValid ? styles['invalid'] : null
|
||||||
|
}`}
|
||||||
|
type={type}
|
||||||
|
placeholder={placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
onBlur={onBlur}
|
||||||
|
/>
|
||||||
|
{isValid && <p className={styles['input-error']}>{isValid}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Input;
|
||||||
69
frontend/src/config/api.js
Normal file
69
frontend/src/config/api.js
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { API_URL } from './env';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
|
const api = axios.create({
|
||||||
|
baseURL: API_URL,
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
api.defaults.withCredentials = true;
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const accessToken = localStorage.getItem('accessToken');
|
||||||
|
const refreshToken = Cookies.get('refreshToken');
|
||||||
|
|
||||||
|
if (accessToken) {
|
||||||
|
config.headers.Authorization = `Bearer ${accessToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (refreshToken) {
|
||||||
|
config.headers['X-Refresh-Token'] = `Bearer ${refreshToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (config.data instanceof FormData) {
|
||||||
|
config.headers['Content-Type'] = 'multipart/form-data';
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response.data,
|
||||||
|
(error) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|
||||||
|
function useApi() {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
async function get(url, options = {}) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.get(url, options);
|
||||||
|
setLoading(false);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function post(url, data) {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const response = await api.post(url, data);
|
||||||
|
setLoading(false);
|
||||||
|
return response;
|
||||||
|
} catch (error) {
|
||||||
|
setLoading(false);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { get, post, loading };
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useApi;
|
||||||
1
frontend/src/config/env.js
Normal file
1
frontend/src/config/env.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const API_URL = 'http://localhost:3000';
|
||||||
2
frontend/src/config/index.js
Normal file
2
frontend/src/config/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './env';
|
||||||
|
export { default as useApi } from './api';
|
||||||
129
frontend/src/context/AuthContext.jsx
Normal file
129
frontend/src/context/AuthContext.jsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import React, { useState, createContext, useEffect } from 'react';
|
||||||
|
import { useHistory } from 'react-router-dom';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { useApi } from '@config';
|
||||||
|
|
||||||
|
const auth = {
|
||||||
|
user: localStorage.getItem('user'),
|
||||||
|
accessToken: null,
|
||||||
|
refreshToken: null,
|
||||||
|
isLogged: false,
|
||||||
|
setLoading: () => {},
|
||||||
|
register: () => {},
|
||||||
|
login: () => {},
|
||||||
|
logout: () => {}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthContext = createContext(auth);
|
||||||
|
|
||||||
|
const AuthProvider = ({ children }) => {
|
||||||
|
const [user, setUser] = useState(auth.user && JSON.parse(auth.user));
|
||||||
|
const [accessToken, setAccessToken] = useState(null);
|
||||||
|
const [refreshToken, setRefreshToken] = useState(null);
|
||||||
|
const { get, post } = useApi();
|
||||||
|
|
||||||
|
const isLoggedIn = !!accessToken;
|
||||||
|
|
||||||
|
const history = useHistory();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const initAuth = async () => {
|
||||||
|
const accessToken = window.localStorage.getItem('accessToken');
|
||||||
|
if (accessToken) {
|
||||||
|
await get('/auth/auth-me')
|
||||||
|
.then(async (response) => {
|
||||||
|
const { user: userData, accessToken: AccessToken } = response.data;
|
||||||
|
if (accessToken) {
|
||||||
|
window.localStorage.setItem('user', JSON.stringify(userData));
|
||||||
|
window.localStorage.setItem('accessToken', AccessToken);
|
||||||
|
}
|
||||||
|
setUser(userData);
|
||||||
|
setAccessToken(AccessToken);
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
localStorage.removeItem('user');
|
||||||
|
localStorage.removeItem('refreshToken');
|
||||||
|
localStorage.removeItem('accessToken');
|
||||||
|
setUser(null);
|
||||||
|
setAccessToken(null);
|
||||||
|
setRefreshToken(null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
initAuth();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogin = async (params, errorCallback) => {
|
||||||
|
try {
|
||||||
|
const response = await post('/auth/login', params);
|
||||||
|
// Extract user, accessToken, refreshToken from the response body
|
||||||
|
const { user, accessToken, refreshToken } = response.data;
|
||||||
|
|
||||||
|
// Store accessToken and refreshToken in state
|
||||||
|
setAccessToken(accessToken);
|
||||||
|
setRefreshToken(refreshToken);
|
||||||
|
|
||||||
|
// Store user in state
|
||||||
|
setUser(user);
|
||||||
|
|
||||||
|
// Set user data in localStorage
|
||||||
|
window.localStorage.setItem('user', JSON.stringify(user));
|
||||||
|
// Set accessToken in localStorage
|
||||||
|
window.localStorage.setItem('accessToken', accessToken);
|
||||||
|
|
||||||
|
// Set refreshToken in cookies
|
||||||
|
Cookies.set('refreshToken', refreshToken, {
|
||||||
|
expires: 30,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict'
|
||||||
|
});
|
||||||
|
history.replace('/');
|
||||||
|
} catch (err) {
|
||||||
|
if (errorCallback) errorCallback(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleRegister = async (params, errorCallback) => {
|
||||||
|
try {
|
||||||
|
const response = await post('/auth/register', params);
|
||||||
|
if (response.data.error) {
|
||||||
|
if (errorCallback) errorCallback(response.data.error);
|
||||||
|
} else {
|
||||||
|
handleLogin({ email: params.email, password: params.password });
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
if (errorCallback) errorCallback(err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
// Remove states
|
||||||
|
setUser(null);
|
||||||
|
setAccessToken(null);
|
||||||
|
setRefreshToken(null);
|
||||||
|
|
||||||
|
// Remove user data from localStorage
|
||||||
|
window.localStorage.removeItem('user');
|
||||||
|
// Remove accessToken from localStorage
|
||||||
|
window.localStorage.removeItem('accessToken');
|
||||||
|
// Remove refreshToken from cookies
|
||||||
|
Cookies.remove('refreshToken', {
|
||||||
|
expires: 30,
|
||||||
|
secure: true,
|
||||||
|
sameSite: 'strict'
|
||||||
|
});
|
||||||
|
history.replace('/login');
|
||||||
|
};
|
||||||
|
|
||||||
|
const values = {
|
||||||
|
user: user,
|
||||||
|
accessToken: accessToken,
|
||||||
|
refreshToken: refreshToken,
|
||||||
|
isLogged: isLoggedIn,
|
||||||
|
login: handleLogin,
|
||||||
|
register: handleRegister,
|
||||||
|
logout: handleLogout
|
||||||
|
};
|
||||||
|
|
||||||
|
return <AuthContext.Provider value={values}>{children}</AuthContext.Provider>;
|
||||||
|
};
|
||||||
|
export default AuthProvider;
|
||||||
2
frontend/src/hooks/index.js
Normal file
2
frontend/src/hooks/index.js
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
import { useAuth } from './useAuth';
|
||||||
|
export { useAuth };
|
||||||
4
frontend/src/hooks/useAuth.js
Normal file
4
frontend/src/hooks/useAuth.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
import { AuthContext } from '@context/AuthContext';
|
||||||
|
|
||||||
|
export const useAuth = () => useContext(AuthContext);
|
||||||
16
frontend/src/main.jsx
Normal file
16
frontend/src/main.jsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import AuthProvider from '@context/AuthContext.jsx';
|
||||||
|
import App from './App.jsx';
|
||||||
|
import '@styles/index.css';
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<BrowserRouter>
|
||||||
|
<AuthProvider>
|
||||||
|
<App />
|
||||||
|
</AuthProvider>
|
||||||
|
</BrowserRouter>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
29
frontend/src/pages/home/index.jsx
Normal file
29
frontend/src/pages/home/index.jsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
import { useAuth } from '@hooks';
|
||||||
|
import styles from '@styles/home.module.css';
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
const { logout } = useAuth();
|
||||||
|
return (
|
||||||
|
<div className={styles['home-page']}>
|
||||||
|
<h2>Welcome</h2>
|
||||||
|
<hr />
|
||||||
|
<p className={styles['localStorage-data']}>
|
||||||
|
<strong>accessToken:</strong> {localStorage.getItem('accessToken')}
|
||||||
|
</p>
|
||||||
|
<p className={styles['localStorage-data']}>
|
||||||
|
<strong>refreshToken:</strong> {Cookies.get('refreshToken')}
|
||||||
|
</p>
|
||||||
|
<p className={styles['localStorage-data']}>
|
||||||
|
<strong>user:</strong>
|
||||||
|
{localStorage.getItem('user')}
|
||||||
|
</p>
|
||||||
|
<hr />
|
||||||
|
<button className={styles['logout-button']} onClick={logout}>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Home;
|
||||||
84
frontend/src/pages/login/index.jsx
Normal file
84
frontend/src/pages/login/index.jsx
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '@hooks';
|
||||||
|
import Input from '@UI/input';
|
||||||
|
import styles from '@styles/form.module.css';
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [values, setValues] = useState({
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
const [error, setError] = useState({
|
||||||
|
email: null,
|
||||||
|
password: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const { login } = useAuth();
|
||||||
|
|
||||||
|
const handleChange = (prop) => (event) => {
|
||||||
|
if (error.email || error.password) {
|
||||||
|
setError({
|
||||||
|
email: null,
|
||||||
|
password: null
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setValues((prev) => ({ ...prev, [prop]: event.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
login(values, (err) => {
|
||||||
|
const errors = err.response.data.errors;
|
||||||
|
errors.forEach((error) => {
|
||||||
|
if (error.email) {
|
||||||
|
setError((prev) => ({ ...prev, email: error.email }));
|
||||||
|
}
|
||||||
|
if (error.password) {
|
||||||
|
setError((prev) => ({ ...prev, password: error.password }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Inputs = [
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
label: 'Email',
|
||||||
|
type: 'email',
|
||||||
|
value: values.email,
|
||||||
|
isValid: error.email,
|
||||||
|
onChange: handleChange('email')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
type: 'password',
|
||||||
|
value: values.password,
|
||||||
|
isValid: error.password,
|
||||||
|
onChange: handleChange('password')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setValues({ email: '', password: '' });
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className={styles['login-page']}>
|
||||||
|
<h2>Login Page</h2>
|
||||||
|
<form onSubmit={onFormSubmit} className={styles['form']}>
|
||||||
|
{Inputs.map((input) => (
|
||||||
|
<Input key={input.id} {...input} />
|
||||||
|
))}
|
||||||
|
<button type='submit'>Log In</button>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
Don't have account? <Link to='/register'>Register</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
114
frontend/src/pages/register/index.jsx
Normal file
114
frontend/src/pages/register/index.jsx
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import React, { useState, useEffect } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
import { useAuth } from '@hooks';
|
||||||
|
import Input from '@UI/input';
|
||||||
|
import styles from '@styles/form.module.css';
|
||||||
|
|
||||||
|
const Register = () => {
|
||||||
|
const [values, setValues] = useState({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
const [error, setError] = useState({
|
||||||
|
first_name: null,
|
||||||
|
last_name: null,
|
||||||
|
email: null,
|
||||||
|
password: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const { register } = useAuth();
|
||||||
|
|
||||||
|
const handleChange = (prop) => (event) => {
|
||||||
|
setValues((prev) => ({ ...prev, [prop]: event.target.value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
register(values, (err) => {
|
||||||
|
const errors = err.response.data.errors;
|
||||||
|
errors.forEach((error) => {
|
||||||
|
if (error.first_name) {
|
||||||
|
setError((prev) => ({ ...prev, first_name: error.first_name }));
|
||||||
|
}
|
||||||
|
if (error.last_name) {
|
||||||
|
setError((prev) => ({ ...prev, last_name: error.last_name }));
|
||||||
|
}
|
||||||
|
if (error.email) {
|
||||||
|
setError((prev) => ({ ...prev, email: error.email }));
|
||||||
|
}
|
||||||
|
if (error.password) {
|
||||||
|
setError((prev) => ({ ...prev, password: error.password }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const Inputs = [
|
||||||
|
{
|
||||||
|
id: 'first_name',
|
||||||
|
label: 'First Name',
|
||||||
|
type: 'text',
|
||||||
|
value: values.first_name,
|
||||||
|
isValid: error.first_name,
|
||||||
|
onChange: handleChange('first_name')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'last_name',
|
||||||
|
label: 'Last Name',
|
||||||
|
type: 'text',
|
||||||
|
value: values.last_name,
|
||||||
|
isValid: error.last_name,
|
||||||
|
onChange: handleChange('last_name')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
label: 'Email Address',
|
||||||
|
type: 'email',
|
||||||
|
value: values.email,
|
||||||
|
isValid: error.email,
|
||||||
|
onChange: handleChange('email')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'password',
|
||||||
|
label: 'New Password',
|
||||||
|
type: 'password',
|
||||||
|
value: values.password,
|
||||||
|
isValid: error.password,
|
||||||
|
onChange: handleChange('password')
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setValues({
|
||||||
|
first_name: '',
|
||||||
|
last_name: '',
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
setError({
|
||||||
|
first_name: null,
|
||||||
|
last_name: null,
|
||||||
|
email: null,
|
||||||
|
password: null
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<div className={styles['register-page']}>
|
||||||
|
<h2>Register Page</h2>
|
||||||
|
<form onSubmit={handleSubmit} className={styles['form']}>
|
||||||
|
{Inputs.map((input) => (
|
||||||
|
<Input key={input.id} {...input} />
|
||||||
|
))}
|
||||||
|
<button type='submit'>Register</button>
|
||||||
|
</form>
|
||||||
|
<p>
|
||||||
|
Already have an account? <Link to='/login'>Login</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export default Register;
|
||||||
56
frontend/src/styles/form.module.css
Normal file
56
frontend/src/styles/form.module.css
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
.login-page,
|
||||||
|
.register-page {
|
||||||
|
transform: translateY(20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.register-page p,
|
||||||
|
.register-page h2,
|
||||||
|
.login-page p,
|
||||||
|
.login-page h2 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
width: 300px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input:focus {
|
||||||
|
border: none;
|
||||||
|
outline: 2px solid #FFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form button {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
font-weight: bold;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
background-color: #FFF;
|
||||||
|
color: #000;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form button:hover {
|
||||||
|
background-color: #ddd;
|
||||||
|
transition: background .5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 480px) {
|
||||||
|
|
||||||
|
.register-page h2,
|
||||||
|
.login-page h2 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.form {
|
||||||
|
width: 100%;
|
||||||
|
margin: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
21
frontend/src/styles/home.module.css
Normal file
21
frontend/src/styles/home.module.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.home-page {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localStorage-data {
|
||||||
|
font-size: 12px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.localStorage-data strong {
|
||||||
|
display: block;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logout-button {
|
||||||
|
display: block;
|
||||||
|
width: 100px;
|
||||||
|
height: 40px;
|
||||||
|
margin: 10px 0px;
|
||||||
|
}
|
||||||
29
frontend/src/styles/index.css
Normal file
29
frontend/src/styles/index.css
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
:root {
|
||||||
|
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||||
|
line-height: 1.5;
|
||||||
|
font-weight: 400;
|
||||||
|
color-scheme: light dark;
|
||||||
|
color: rgba(255, 255, 255, 0.87);
|
||||||
|
background-color: #242424;
|
||||||
|
font-synthesis: none;
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
-moz-box-sizing: border-box;
|
||||||
|
-webkit-box-sizing: border-box;
|
||||||
|
-ms-box-sizing: border-box;
|
||||||
|
-o-box-sizing: border-box;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
font-weight: 500;
|
||||||
|
color: inherit;
|
||||||
|
text-decoration: inherit;
|
||||||
|
}
|
||||||
21
frontend/src/styles/input.module.css
Normal file
21
frontend/src/styles/input.module.css
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
.input-box {
|
||||||
|
margin: 4px 0px;
|
||||||
|
}
|
||||||
|
.input-box_label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box_input {
|
||||||
|
height: 35px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
padding: 0px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-box_input.invalid {
|
||||||
|
border: 1px solid red
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-error {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
19
frontend/vite.config.js
Normal file
19
frontend/vite.config.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import path from 'path';
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@styles': path.resolve(__dirname, 'src/styles'),
|
||||||
|
'@common': path.resolve(__dirname, 'src/components/common'),
|
||||||
|
'@UI': path.resolve(__dirname, 'src/components/UI'),
|
||||||
|
'@hooks': path.resolve(__dirname, 'src/hooks'),
|
||||||
|
'@config': path.resolve(__dirname, 'src/config'),
|
||||||
|
'@pages': path.resolve(__dirname, 'src/pages'),
|
||||||
|
'@context': path.resolve(__dirname, 'src/context')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
2022
frontend/yarn.lock
Normal file
2022
frontend/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,35 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
import { body, oneOf } from 'express-validator';
|
|
||||||
import { validate } from '../validationResult';
|
|
||||||
|
|
||||||
export const validateLogin = [
|
|
||||||
oneOf(
|
|
||||||
[
|
|
||||||
body('username')
|
|
||||||
.exists()
|
|
||||||
.withMessage('username is missing from the body')
|
|
||||||
.notEmpty()
|
|
||||||
.withMessage('username is empty')
|
|
||||||
.isString()
|
|
||||||
.withMessage('username must be string'),
|
|
||||||
body('email')
|
|
||||||
.exists()
|
|
||||||
.withMessage('Email is missing from the body')
|
|
||||||
.notEmpty()
|
|
||||||
.withMessage('Email is empty')
|
|
||||||
.isEmail()
|
|
||||||
.withMessage('Email is not valid')
|
|
||||||
.normalizeEmail()
|
|
||||||
.withMessage('Email is not normalized')
|
|
||||||
],
|
|
||||||
'Must be at least one of email or username'
|
|
||||||
),
|
|
||||||
body('password')
|
|
||||||
.exists()
|
|
||||||
.withMessage('password is missing from the body')
|
|
||||||
.notEmpty()
|
|
||||||
.withMessage('password is empty')
|
|
||||||
.isLength({ min: 8 })
|
|
||||||
.withMessage('password must be at least 8 characters long'),
|
|
||||||
(req: Request, res: Response, next: NextFunction) => validate(req, res, next)
|
|
||||||
];
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
import bcrypt from 'bcrypt';
|
|
||||||
import configs from '../configs';
|
|
||||||
|
|
||||||
export const hash = (password: string) =>
|
|
||||||
bcrypt.hash(`${configs.pepper}${password}${configs.pepper}`, configs.salt);
|
|
||||||
|
|
||||||
export const compare = (password: string, hash: string) =>
|
|
||||||
bcrypt.compare(`${configs.pepper}${password}${configs.pepper}`, hash);
|
|
||||||
Loading…
Reference in New Issue
Block a user