add user session
This commit is contained in:
@@ -1,12 +1,16 @@
|
||||
import { RequestHandler, Router } from 'express'
|
||||
import { Router } from 'express';
|
||||
import { UserDto } from '../dto/user.dto';
|
||||
import { AuthService } from '../service/auth.service';
|
||||
import { UserService } from '../service/user.service';
|
||||
import { createPath as buildPath } from '../utils/createPath';
|
||||
|
||||
const router = Router();
|
||||
|
||||
const basePath = '/users'
|
||||
const BASE_PATH = '/users'
|
||||
|
||||
export const post: RequestHandler = (req, res) => {
|
||||
export const createPath = BASE_PATH
|
||||
|
||||
router.post(createPath, (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
UserService.create({
|
||||
@@ -24,7 +28,46 @@ export const post: RequestHandler = (req, res) => {
|
||||
error: err.message
|
||||
});
|
||||
})
|
||||
}
|
||||
router.post(basePath, post)
|
||||
})
|
||||
|
||||
export const signInPath = buildPath(BASE_PATH, 'sign_in')
|
||||
router.post(signInPath, async (req, res) => {
|
||||
const { email, password } = req.body;
|
||||
|
||||
const user = await UserService.findByEmail(email)
|
||||
|
||||
const isPasswordValid = await AuthService.isUserPasswordValid(user, password)
|
||||
|
||||
if (isPasswordValid) {
|
||||
const token = await AuthService.createSession(user)
|
||||
|
||||
res.json({
|
||||
auth: true,
|
||||
token
|
||||
})
|
||||
} else {
|
||||
res.status(500).json({
|
||||
auth: false,
|
||||
token: null,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const signOutPath = buildPath(BASE_PATH, 'sign_out')
|
||||
router.delete(signOutPath, async (req, res) => {
|
||||
const token = req.headers['x-access-token']
|
||||
|
||||
if (typeof token === 'string') {
|
||||
await AuthService.destoySession(token)
|
||||
|
||||
res.status(204).json({
|
||||
success: true
|
||||
})
|
||||
} else {
|
||||
res.status(422).json({
|
||||
success: false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
export const UserRoutes = router;
|
||||
3
server/src/dto/session.dto.ts
Normal file
3
server/src/dto/session.dto.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
type SessionDto = {
|
||||
userEmail: string;
|
||||
}
|
||||
5
server/src/express.d.ts
vendored
Normal file
5
server/src/express.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace Express {
|
||||
export interface Request {
|
||||
userId?: number
|
||||
}
|
||||
}
|
||||
@@ -3,18 +3,33 @@ import * as dotenv from 'dotenv';
|
||||
import * as express from 'express';
|
||||
import { AppDataSource } from "./infra/dataSource";
|
||||
import { UserRoutes } from "./controller";
|
||||
import { RedisConnection } from "./infra/redis";
|
||||
import { sessionMiddleware } from "./middleware/session.middleware";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.use(sessionMiddleware)
|
||||
app.use(UserRoutes)
|
||||
|
||||
AppDataSource.initialize().then(() => {
|
||||
|
||||
const startApp = async () => {
|
||||
console.log('[redis]: connecting')
|
||||
await RedisConnection.connect()
|
||||
console.log('[redis]: connected')
|
||||
|
||||
console.log('[database]: connecting')
|
||||
await AppDataSource.initialize()
|
||||
console.log('[database]: connected')
|
||||
|
||||
const port = process.env.PORT;
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running at ${port} 🚀`);
|
||||
console.log(`[server] is running at ${port} 🚀`);
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
startApp()
|
||||
5
server/src/infra/redis.ts
Normal file
5
server/src/infra/redis.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { createClient } from 'redis';
|
||||
|
||||
export const RedisConnection = createClient();
|
||||
|
||||
RedisConnection.on('error', (err) => console.log('Redis Client Error', err));
|
||||
8
server/src/middleware/__test__/unprotectedRoutes.spec.ts
Normal file
8
server/src/middleware/__test__/unprotectedRoutes.spec.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { signInPath } from '../../controller/users.controller';
|
||||
import { UNPROTECTED_ROUTES } from '../session.middleware'
|
||||
|
||||
describe('Unprotected Routes', () => {
|
||||
it('check content', () => {
|
||||
expect(UNPROTECTED_ROUTES.sort()).toEqual([signInPath].sort());
|
||||
})
|
||||
})
|
||||
28
server/src/middleware/session.middleware.ts
Normal file
28
server/src/middleware/session.middleware.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { Handler, Request, Response } from 'express';
|
||||
import { verify } from 'jsonwebtoken';
|
||||
import { signInPath } from '../controller/users.controller';
|
||||
import { AuthService } from '../service/auth.service';
|
||||
|
||||
export const UNPROTECTED_ROUTES = [signInPath];
|
||||
|
||||
export const sessionMiddleware: Handler = (req: Request, res: Response, next) => {
|
||||
const token = req.headers['x-access-token'];
|
||||
|
||||
if (UNPROTECTED_ROUTES.includes(req.url)) {
|
||||
next();
|
||||
} else if (typeof token === 'string') {
|
||||
AuthService.isSessionValid(token).then(valid => {
|
||||
verify(token, process.env.SECRET, function (err, decoded) {
|
||||
if (err || !valid) return res.status(500).json({ auth: false, message: 'Failed to authenticate token.' });
|
||||
|
||||
if (!(typeof decoded === 'string')) {
|
||||
req.userId = +decoded.sub;
|
||||
}
|
||||
|
||||
next();
|
||||
});
|
||||
})
|
||||
} else {
|
||||
return res.status(401).json({ auth: false, message: 'No token provided.' })
|
||||
}
|
||||
}
|
||||
25
server/src/repository/session.repository.ts
Normal file
25
server/src/repository/session.repository.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { RedisConnection } from "../infra/redis"
|
||||
|
||||
const sessionExists = async (jwt: string): Promise<boolean> => {
|
||||
const result = await RedisConnection.get(jwt)
|
||||
|
||||
return result == "valid"
|
||||
}
|
||||
|
||||
const saveSession = async (jwt: string): Promise<boolean> => {
|
||||
const result = await RedisConnection.set(jwt, 'valid')
|
||||
|
||||
return result == "valid"
|
||||
}
|
||||
|
||||
const deleteSession = async (jwt: string): Promise<boolean> => {
|
||||
const result = await RedisConnection.del(jwt)
|
||||
|
||||
return !!result
|
||||
}
|
||||
|
||||
export const sessionRepository = {
|
||||
sessionExists,
|
||||
saveSession,
|
||||
deleteSession,
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { User } from "../entity/user.entity";
|
||||
import * as bcrypt from 'bcrypt'
|
||||
import { JwtPayload, sign as JwtSign } from "jsonwebtoken";
|
||||
import { sessionRepository } from "../repository/session.repository";
|
||||
|
||||
export const SALT_ROUNDS = 10
|
||||
|
||||
@@ -29,7 +31,32 @@ const isUserPasswordValid = (user: User, password: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const isSessionValid = (token: string): Promise<boolean> => {
|
||||
return sessionRepository.sessionExists(token)
|
||||
}
|
||||
|
||||
const createSession = async (user: User): Promise<string> => {
|
||||
const payload: JwtPayload = {
|
||||
sub: user.id.toString(),
|
||||
}
|
||||
|
||||
const token = JwtSign(payload, process.env.SECRET);
|
||||
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
sessionRepository.saveSession(token)
|
||||
.then(() => resolve(token))
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
const destoySession = (token: string): Promise<boolean> => {
|
||||
return sessionRepository.deleteSession(token)
|
||||
}
|
||||
|
||||
export const AuthService = {
|
||||
updateUserPassword,
|
||||
isUserPasswordValid
|
||||
isUserPasswordValid,
|
||||
isSessionValid,
|
||||
createSession,
|
||||
destoySession
|
||||
}
|
||||
@@ -31,7 +31,12 @@ async function findByEmail(email: string): Promise<User | undefined> {
|
||||
return userRepository.findOneBy({ email })
|
||||
}
|
||||
|
||||
async function findUserById(id: number): Promise<User | undefined> {
|
||||
return userRepository.findOneBy({ id })
|
||||
}
|
||||
|
||||
export const UserService = {
|
||||
create,
|
||||
findByEmail
|
||||
findByEmail,
|
||||
findUserById
|
||||
}
|
||||
3
server/src/utils/createPath.ts
Normal file
3
server/src/utils/createPath.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const createPath = (...args: string[]): string => {
|
||||
return args.join('/')
|
||||
}
|
||||
Reference in New Issue
Block a user