[TIL] HW5 Refactoring - 1
04/26/23
![[TIL] HW5 Refactoring - 1](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1682591417148%2F9d38ebea-c277-44f7-b638-faade818771f.png&w=3840&q=75)
1. How to Store Refresh Token in Redis
Related Post: How to store refresh token in Redis
I tried to store the refresh token in the Redis cloud.
login API before refactoring
const redisClient = redis.createClient({
url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
legacyMode: true,
});
redisClient.on("connect", () => {
console.info("Redis connected!");
});
redisClient.on("error", (err) => {
console.error("Redis Client Error", err);
});
redisClient.connect().then();
const redisCli = redisClient.v4;
await redisCli.set(refreshToken, userId);
res.cookie("accessToken", `Bearer ${accessToken}`);
res.cookie("refreshToken", `Bearer ${refreshToken}`);
I tried implementing a login page where accessToken and refreshToken are reissued in the following manner:
First, I created a redisClient and established a connection.
I used the set(key, value) method to store the refreshToken in Redis as the key and the userId as the value.
I stored the access and refresh tokens in the cookie.
auth-middleware before refactoring
const redisClient = redis.createClient({
url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
legacyMode: true,
});
redisClient.on("connect", () => {
console.info("Redis connected!");
});
redisClient.on("error", (err) => {
console.error("Redis Client Error", err);
});
redisClient.connect().then();
const redisCli = redisClient.v4;
if (!isRefreshTokenValid) {
await redisCli.del(authRefreshToken);
return res
.status(419)
.json({ message: "Refresh Token has been expired." });
}
if (!isAccessTokenValid) {
const accessTokenId = await redisCli.get(authRefreshToken);
if (!accessTokenId)
return res.status(419).json({
message: "Refresh Token does not exist in the server.",
});
newAccessToken = createAccessToken(accessTokenId);
res.cookie("accessToken", `Bearer ${newAccessToken}`);
return res.json({ message: "Access Token is newly generated. Please try again." });
}
const { userId } = getAccessTokenPayload(authAccessToken);
const user = await Users.findOne({ where: { userId } });
res.locals.user = user
next();
In the auth-middleware, we check if the access and refresh tokens are valid, and if the access token has expired, we regenerate it. Here are the steps we take:
First, we create a redisClient and connect it to Redis.
We perform token validation. If the refreshToken has already expired, we remove its key-value pair from Redis. We retrieve the userId information from Redis using the refreshToken, and if the access token has expired, we regenerate it.
We extract the userId payload from the access token and store it in res.locals.user.
We pass the request to the next middleware.
When creating the Redis connection, the code was duplicated. Therefore, refactoring was done by declaring a class defining functions related to Redis.
Refactoring by creating RedisClient Class
class RedisClient {
constructor() {
this.redisClient = redis.createClient({
url: `redis://${process.env.REDIS_USERNAME}:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}/0`,
legacyMode: true,
});
this.redisConnected = false;
}
async initialize() {
this.redisClient.on("connect", () => {
this.redisConnected = true;
console.info("Redis connected!");
});
this.redisClient.on("error", (err) => {
console.error("Redis Client Error", err);
});
if (!this.redisConnected) this.redisClient.connect().then();
}
setRefreshToken = async (refreshToken, userId) => {
await this.initialize();
await this.redisClient.set(refreshToken, userId);
};
getRefreshToken = async (refreshToken) => {
await this.initialize();
const token = await this.redisClient.get(refreshToken);
return token;
};
deleteRefreshToken = async (refreshToken) => {
await this.initialize();
await this.redisClient.del(refreshToken);
};
}
An error occurred when logging in again after logging in once, which was
throw new Error('Socket already opened'); Error: Socket already opened error. This was caused by trying to reopen the Redis connection even though it was already connected. To fix this error, a boolean variable namedthis.redisConnectedwas created in the constructor, and Redis connection was only attempted when this value was false.In the login API and auth-middleware, an instance was created using
redisClient = new RedisClient();and existing Redis methods were replaced with functions defined in the class.
Use 3-layered Architecture
Repository Layer (actual layer accessing Redis): The
RedisClientclass is put into the users repository, and two classes are exported using objects.module.exports = { UserRepository, RedisClientRepository };Service Layer:
RedisClientRepositoryis imported and related functions are defined. If data processing is required, it is done in this layer.Controller Layer:
RedisClientServiceis imported, and an instance is created within the UserController class.redisClient = new RedisClientService();The Redis methods are used in this layer through the instance.
Body data validation using joi module
Before Refactoring
Validation using if statements
if (typeof nickname !== "string")
throw new Error("412/Nickname format is incorrect.");
if (password !== confirmedPassword)
throw new Error("412/Passwords are not matching.");
if (password.length < 4 || typeof password !== "string")
throw new Error("412/Password format is incorrect.");
if (password.includes(nickname.toLowerCase()))
throw new Error("412/Password includes nickname.");
const nickNameRegex = new RegExp("^[a-zA-z0-9]{3,}$", "g");
if (!nickNameRegex.test(nickname))
throw new Error("412/Nickname format is incorrect.");
After refactoring: joi
const { nickname, password } = await signupSchema
.validateAsync(req.body)
.catch((error) => {
console.error(error);
throw new Error(`412/${error}`);
});
joi.jsfile
signupSchema: Joi.object({
nickname: Joi.string()
.regex(/^[a-zA-Z0-9]{3,}$/)
.messages({
"string.base": "Nickname format is incorrect.",
"string.pattern.base": "Nickname format is incorrect.",
"string.empty": "Nickname format is incorrect.",
"string.min": "Nickname format is incorrect.",
}),
password: Joi.string().min(4).required().messages({
"string.base": "Password format is incorrect.",
"string.empty": "Password format is incorrect.",
"string.min": "Password format is incorrect.",
}),
confirmedPassword: Joi.string().valid(Joi.ref("password")).required().messages({
"string.base": "Passwords are not matching.",
"any.only": "Passwords are not matching.",
"string.empty": "Passwords are not matching.",
}),
}),s
![[코테] 그리디 문제 - 무지의 먹방 라이브](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1712215455263%2F1ac1f35a-8862-4e42-8d0c-e2bea01e04c0.png&w=3840&q=75)
![[코테] Bfs 토마토](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1709032619170%2F70056896-c857-444b-9c99-45bfcb466806.png&w=3840&q=75)
![[코테] Dfs 문제 유형 - 그래프 내에서 구분하여 카운트 하기](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1709019361383%2Fb0585d72-c808-4169-83a9-2724f312e927.png&w=3840&q=75)
![[코테] DFS vs BFS](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708971211123%2F71f9386c-6a62-43b2-a602-4d084c24d6cf.png&w=3840&q=75)
![[코테] 여행경로](/_next/image?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1708971251412%2F27ce72ed-8ee7-4d13-a02f-ff4bbe50c4be.png&w=3840&q=75)