{"id":394127,"date":"2024-06-29T11:25:10","date_gmt":"2024-06-29T11:25:10","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=394127"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=394127","title":{"rendered":"<span>You don&#8217;t know Redis (Part 2)<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>In the first part of\u00a0<a href=\"https:\/\/habr.com\/en\/post\/564234\/\" rel=\"noopener noreferrer nofollow\"><u>You don&#8217;t know Redis<\/u><\/a>, I built an app using Redis as a primary database. For most people, it might sound unusual simply because the key-value data structure seems suboptimal for handling complex data models.<\/p>\n<p>In practice, the choice of a database often depends on the application\u2019s data-access patterns as well as the current and possible future requirements.<\/p>\n<p>Redis was a perfect database for a\u00a0<a href=\"https:\/\/techforitrecruiters.com\/questions\/active\" rel=\"noopener noreferrer nofollow\"><u>Q&amp;A board<\/u><\/a>. I described how I took advantage of\u00a0<a href=\"https:\/\/redis.io\/topics\/data-types#sorted-sets\" rel=\"noopener noreferrer nofollow\"><u>sorted sets<\/u><\/a>\u00a0and\u00a0<a href=\"https:\/\/redis.io\/topics\/data-types#hashes\" rel=\"noopener noreferrer nofollow\"><u>hashes<\/u><\/a>\u00a0data types to build features efficiently with less code.<\/p>\n<p>Now I need to extend the Q&amp;A board with registration\/login functionality.<\/p>\n<p>I will use Redis again. There are two reasons for that.<\/p>\n<p>Firstly, I want to avoid the extra complexity that comes with adding yet another database.<\/p>\n<p>Secondly, based on the requirements that I have, Redis is suitable for the task.<\/p>\n<p>Important to note, that user registration and login is not always about only email and password handling. Users may have a lot of relations with other data which can grow complex over time.<\/p>\n<p>Despite Redis being suitable for my task, it may not be a good choice for other projects.<\/p>\n<p>Always define what data structure you need now and may need in the future to pick the right database.<\/p>\n<h3>Implementation<\/h3>\n<p>I use serverless functions, the\u00a0<a href=\"https:\/\/github.com\/luin\/ioredis\" rel=\"noopener noreferrer nofollow\"><u>ioredis<\/u><\/a>\u00a0library and\u00a0<a href=\"https:\/\/upstash.com\/?utm_source=sndr_3\" rel=\"noopener noreferrer nofollow\"><u>Upstash Serverless Redis<\/u><\/a>.<\/p>\n<p>I can\u2019t help but talk about serverless all the time because it greatly simplifies development. I love when complexity is removed whenever possible and Upstash is doing just that for me.<\/p>\n<p>I have zero work with setting up Redis. Moreover, I am using Upstash both in development and production.<\/p>\n<h3>Registration flow<\/h3>\n<p>During registration, we collect the user\u00a0<code>name<\/code>,\u00a0<code>email<\/code>\u00a0and\u00a0<code>password<\/code>. Before registering a user, we need to make sure that the email has not been registered already (is unique in the system).<\/p>\n<p>Redis does not support constraints. However, we can keep track of all registered emails using a sorted set named\u00a0<code>emails<\/code>.<\/p>\n<p>On every new registration, we can use the\u00a0<a href=\"https:\/\/redis.io\/commands\/zscore\" rel=\"noopener noreferrer nofollow\"><u>ZSCORE<\/u><\/a>\u00a0command to check whether the provided email is already registered.<\/p>\n<p>If the email is taken, we need to notify the user about it.<\/p>\n<p>\u200b Note, that this isn\u2019t the best option because by telling that a given email is registered we provide a simple way for anyone to check whether someone is registered with a particular service, albeit it\u2019s not a big security issue.<\/p>\n<p>Before we can save a new user, we need to:<\/p>\n<ul>\n<li>\n<p><strong>Generate a unique user\u00a0<\/strong><code>ID<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>We can use the\u00a0<a href=\"https:\/\/redis.io\/commands\/incr\" rel=\"noopener noreferrer nofollow\"><u>INCR<\/u><\/a>\u00a0command to always get a unique value by incrementing a number stored at a key by one. If the key does not exist, Redis will set it to\u00a0<code>0<\/code>\u00a0before performing the operation. This means that the initial value will be\u00a0<code>1<\/code>.<\/p>\n<pre><code>const id = await redis.incr('user_ids') \/\/ -> 1<\/code><\/pre>\n<p>Whenever you need to create a\u00a0<a href=\"https:\/\/redis.io\/commands\/incr#pattern-counter\" rel=\"noopener noreferrer nofollow\"><u>counter<\/u><\/a>,\u00a0<code>INCR<\/code>\u00a0is a great choice. Or you can build a\u00a0<a href=\"https:\/\/redis.io\/commands\/incr#pattern-rate-limiter\" rel=\"noopener noreferrer nofollow\"><u>rate-limiter<\/u><\/a>\u00a0to protect your API from being overwhelmed by using\u00a0<code>INCR<\/code>\u00a0together with\u00a0<a href=\"https:\/\/redis.io\/commands\/expire\" rel=\"noopener noreferrer nofollow\"><u>EXPIRE<\/u><\/a>.<\/p>\n<ul>\n<li>\n<p><strong>Hash the password with the\u00a0<\/strong><a href=\"https:\/\/github.com\/kelektiv\/node.bcrypt.js\/\" rel=\"noopener noreferrer nofollow\"><strong><u>bcrypt<\/u><\/strong><\/a><strong>\u00a0library.<\/strong><\/p>\n<\/li>\n<\/ul>\n<pre><code>const hash = await bcrypt.hash(password, 10)<\/code><\/pre>\n<p>Now that we have the unique user\u00a0<code>ID<\/code>\u00a0(e.g. user ID is 7) and the hashed password, we can:<br \/><strong>1. Store user details in a hash under the\u00a0<\/strong><code>user:{ID}<\/code>\u00a0key.<\/p>\n<pre><code>redis.hmset('user:7', { 7, name, email, hash })<\/code><\/pre>\n<p>Knowing the\u00a0<code>ID<\/code>, we can easily get all user details using the\u00a0<a href=\"https:\/\/redis.io\/commands\/hgetall\" rel=\"noopener noreferrer nofollow\"><u>HGETALL<\/u><\/a>\u00a0command:<\/p>\n<pre><code>redis.hgetall('user:7');<\/code><\/pre>\n<p><strong>2. Add the user\u2019s email to the\u00a0<\/strong><code>emails<\/code>\u00a0sorted set.<\/p>\n<pre><code>redis.zadd('emails', -Math.abs(7), email)<\/code><\/pre>\n<p>This allows us to lookup emails to check if they are registered or get the user&#8217;s\u00a0<code>ID<\/code>\u00a0by\u00a0<code>email<\/code>\u00a0which is exactly what we need for the login process.<\/p>\n<p><code>redis.zscore('emails', email)<\/code>\u00a0will return the score which is the\u00a0<code>ID<\/code>\u00a0or\u00a0<code>nil<\/code>\u00a0if the email is not found.<\/p>\n<p>Notice how we use this sorted set for two important features, namely ensuring unique emails and looking up users by email.<\/p>\n<p>But we are taking it one step further and set scores (which represent user\u00a0<code>ID<\/code>s) as negative numbers to mark emails as unverified:\u00a0<code>-Math.abs(7)<\/code>. Then, when the email is verified, we simply convert it to a positive number.<\/p>\n<pre><code>redis.zadd('emails', Math.abs(7), email)<\/code><\/pre>\n<p>If a specified\u00a0<code>email<\/code>\u00a0is already a member of the\u00a0<code>emails<\/code>\u00a0sorted set, Redis will update the score only.<\/p>\n<p>During the login process, we can always check for negative numbers and request users to verify their email instead of logging them in.<\/p>\n<p>Retrieving all unverified emails is a trivial operation done with the\u00a0<a href=\"https:\/\/redis.io\/commands\/zrangebyscore\" rel=\"noopener noreferrer nofollow\"><u>ZRANGEBYSCORE<\/u><\/a>\u00a0command.<\/p>\n<pre><code>redis.zrangebyscore('emails', '-inf', -1, 'WITHSCORES');<\/code><\/pre>\n<p>Registration function\u00a0<a href=\"https:\/\/github.com\/sandorTuranszky\/questions-and-answers-board-built-with-redis\/blob\/main\/lambda\/register.js\" rel=\"noopener noreferrer nofollow\"><u>source code<\/u><\/a><\/p>\n<h3>Login flow<\/h3>\n<p>Before logging in the user, we check if the provided email exists in our database. As mentioned before, the\u00a0<code>score<\/code>\u00a0is the user\u00a0<code>ID<\/code>.<\/p>\n<pre><code>const userId = await redis.zscore('emails', email);<\/code><\/pre>\n<p>If so, we first check if the email is verified by making sure the\u00a0<code>ID<\/code>\u00a0is a positive number. If not, we ask users to verify their email.<\/p>\n<p>If the email is verified, we get the password hash that we stored for the user:<\/p>\n<pre><code>const hash = await redis.hget('user:7', 'hash');<\/code><\/pre>\n<p>and check whether the password is correct:<\/p>\n<pre><code>const match = await bcrypt.compare(password, hash);<\/code><\/pre>\n<p>If the password is correct, we generate a token and return it to the client.<\/p>\n<p>And we are done.<\/p>\n<p>Login function\u00a0<a href=\"https:\/\/github.com\/sandorTuranszky\/questions-and-answers-board-built-with-redis\/blob\/main\/lambda\/login.js\" rel=\"noopener noreferrer nofollow\"><u>source code<\/u><\/a><\/p>\n<h3>Conclusion<\/h3>\n<p>As you can see, we needed four Redis commands for registration and only two for login.<\/p>\n<p>Probably you noticed that while describing the registration and login process with Redis we also revealed two more use cases for Redis, namely counter and rate-limiting.<\/p>\n<p>Redis has a lot more\u00a0<a href=\"https:\/\/redislabs.com\/redis-best-practices\/introduction\/\" rel=\"noopener noreferrer nofollow\"><u>use cases<\/u><\/a>\u00a0beyond cache and learning about them will only make you even more efficient.<\/p>\n<p>Follow me to read about how I am implementing a secure production-ready registration flow with email verification and password recovery backed by Redis.<\/p>\n<hr\/>\n<p>Check out my article on how I implemented the\u00a0<a href=\"https:\/\/habr.com\/en\/post\/568872\/\" rel=\"noopener noreferrer nofollow\"><u>LinkedIn-like reactions with Serverless Redis<\/u><\/a>.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/569178\/\"> https:\/\/habr.com\/ru\/articles\/569178\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>In the first part of\u00a0<a href=\"https:\/\/habr.com\/en\/post\/564234\/\" rel=\"noopener noreferrer nofollow\"><u>You don&#8217;t know Redis<\/u><\/a>, I built an app using Redis as a primary database. For most people, it might sound unusual simply because the key-value data structure seems suboptimal for handling complex data models.<\/p>\n<p>In practice, the choice of a database often depends on the application\u2019s data-access patterns as well as the current and possible future requirements.<\/p>\n<p>Redis was a perfect database for a\u00a0<a href=\"https:\/\/techforitrecruiters.com\/questions\/active\" rel=\"noopener noreferrer nofollow\"><u>Q&amp;A board<\/u><\/a>. I described how I took advantage of\u00a0<a href=\"https:\/\/redis.io\/topics\/data-types#sorted-sets\" rel=\"noopener noreferrer nofollow\"><u>sorted sets<\/u><\/a>\u00a0and\u00a0<a href=\"https:\/\/redis.io\/topics\/data-types#hashes\" rel=\"noopener noreferrer nofollow\"><u>hashes<\/u><\/a>\u00a0data types to build features efficiently with less code.<\/p>\n<p>Now I need to extend the Q&amp;A board with registration\/login functionality.<\/p>\n<p>I will use Redis again. There are two reasons for that.<\/p>\n<p>Firstly, I want to avoid the extra complexity that comes with adding yet another database.<\/p>\n<p>Secondly, based on the requirements that I have, Redis is suitable for the task.<\/p>\n<p>Important to note, that user registration and login is not always about only email and password handling. Users may have a lot of relations with other data which can grow complex over time.<\/p>\n<p>Despite Redis being suitable for my task, it may not be a good choice for other projects.<\/p>\n<p>Always define what data structure you need now and may need in the future to pick the right database.<\/p>\n<h3>Implementation<\/h3>\n<p>I use serverless functions, the\u00a0<a href=\"https:\/\/github.com\/luin\/ioredis\" rel=\"noopener noreferrer nofollow\"><u>ioredis<\/u><\/a>\u00a0library and\u00a0<a href=\"https:\/\/upstash.com\/?utm_source=sndr_3\" rel=\"noopener noreferrer nofollow\"><u>Upstash Serverless Redis<\/u><\/a>.<\/p>\n<p>I can\u2019t help but talk about serverless all the time because it greatly simplifies development. I love when complexity is removed whenever possible and Upstash is doing just that for me.<\/p>\n<p>I have zero work with setting up Redis. Moreover, I am using Upstash both in development and production.<\/p>\n<h3>Registration flow<\/h3>\n<p>During registration, we collect the user\u00a0<code>name<\/code>,\u00a0<code>email<\/code>\u00a0and\u00a0<code>password<\/code>. Before registering a user, we need to make sure that the email has not been registered already (is unique in the system).<\/p>\n<p>Redis does not support constraints. However, we can keep track of all registered emails using a sorted set named\u00a0<code>emails<\/code>.<\/p>\n<p>On every new registration, we can use the\u00a0<a href=\"https:\/\/redis.io\/commands\/zscore\" rel=\"noopener noreferrer nofollow\"><u>ZSCORE<\/u><\/a>\u00a0command to check whether the provided email is already registered.<\/p>\n<p>If the email is taken, we need to notify the user about it.<\/p>\n<p>\u200b Note, that this isn\u2019t the best option because by telling that a given email is registered we provide a simple way for anyone to check whether someone is registered with a particular service, albeit it\u2019s not a big security issue.<\/p>\n<p>Before we can save a new user, we need to:<\/p>\n<ul>\n<li>\n<p><strong>Generate a unique user\u00a0<\/strong><code>ID<\/code>.<\/p>\n<\/li>\n<\/ul>\n<p>We can use the\u00a0<a href=\"https:\/\/redis.io\/commands\/incr\" rel=\"noopener noreferrer nofollow\"><u>INCR<\/u><\/a>\u00a0command to always get a unique value by incrementing a number stored at a key by one. If the key does not exist, Redis will set it to\u00a0<code>0<\/code>\u00a0before performing the operation. This means that the initial value will be\u00a0<code>1<\/code>.<\/p>\n<pre><code>const id = await redis.incr('user_ids') \/\/ -> 1<\/code><\/pre>\n<p>Whenever you need to create a\u00a0<a href=\"https:\/\/redis.io\/commands\/incr#pattern-counter\" rel=\"noopener noreferrer nofollow\"><u>counter<\/u><\/a>,\u00a0<code>INCR<\/code>\u00a0is a great choice. Or you can build a\u00a0<a href=\"https:\/\/redis.io\/commands\/incr#pattern-rate-limiter\" rel=\"noopener noreferrer nofollow\"><u>rate-limiter<\/u><\/a>\u00a0to protect your API from being overwhelmed by using\u00a0<code>INCR<\/code>\u00a0together with\u00a0<a href=\"https:\/\/redis.io\/commands\/expire\" rel=\"noopener noreferrer nofollow\"><u>EXPIRE<\/u><\/a>.<\/p>\n<ul>\n<li>\n<p><strong>Hash the password with the\u00a0<\/strong><a href=\"https:\/\/github.com\/kelektiv\/node.bcrypt.js\/\" rel=\"noopener noreferrer nofollow\"><strong><u>bcrypt<\/u><\/strong><\/a><strong>\u00a0library.<\/strong><\/p>\n<\/li>\n<\/ul>\n<pre><code>const hash = await bcrypt.hash(password, 10)<\/code><\/pre>\n<p>Now that we have the unique user\u00a0<code>ID<\/code>\u00a0(e.g. user ID is 7) and the hashed password, we can:<br \/><strong>1. Store user details in a hash under the\u00a0<\/strong><code>user:{ID}<\/code>\u00a0key.<\/p>\n<pre><code>redis.hmset('user:7', { 7, name, email, hash })<\/code><\/pre>\n<p>Knowing the\u00a0<code>ID<\/code>, we can easily get all user details using the\u00a0<a href=\"https:\/\/redis.io\/commands\/hgetall\" rel=\"noopener noreferrer nofollow\"><u>HGETALL<\/u><\/a>\u00a0command:<\/p>\n<pre><code>redis.hgetall('user:7');<\/code><\/pre>\n<p><strong>2. Add the user\u2019s email to the\u00a0<\/strong><code>emails<\/code>\u00a0sorted set.<\/p>\n<pre><code>redis.zadd('emails', -Math.abs(7), email)<\/code><\/pre>\n<p>This allows us to lookup emails to check if they are registered or get the user&#8217;s\u00a0<code>ID<\/code>\u00a0by\u00a0<code>email<\/code>\u00a0which is exactly what we need for the login process.<\/p>\n<p><code>redis.zscore('emails', email)<\/code>\u00a0will return the score which is the\u00a0<code>ID<\/code>\u00a0or\u00a0<code>nil<\/code>\u00a0if the email is not found.<\/p>\n<p>Notice how we use this sorted set for two important features, namely ensuring unique emails and looking up users by email.<\/p>\n<p>But we are taking it one step further and set scores (which represent user\u00a0<code>ID<\/code>s) as negative numbers to mark emails as unverified:\u00a0<code>-Math.abs(7)<\/code>. Then, when the email is verified, we simply convert it to a positive number.<\/p>\n<pre><code>redis.zadd('emails', Math.abs(7), email)<\/code><\/pre>\n<p>If a specified\u00a0<code>email<\/code>\u00a0is already a member of the\u00a0<code>emails<\/code>\u00a0sorted set, Redis will update the score only.<\/p>\n<p>During the login process, we can always check for negative numbers and request users to verify their email instead of logging them in.<\/p>\n<p>Retrieving all unverified emails is a trivial operation done with the\u00a0<a href=\"https:\/\/redis.io\/commands\/zrangebyscore\" rel=\"noopener noreferrer nofollow\"><u>ZRANGEBYSCORE<\/u><\/a>\u00a0command.<\/p>\n<pre><code>redis.zrangebyscore('emails', '-inf', -1, 'WITHSCORES');<\/code><\/pre>\n<p>Registration function\u00a0<a href=\"https:\/\/github.com\/sandorTuranszky\/questions-and-answers-board-built-with-redis\/blob\/main\/lambda\/register.js\" rel=\"noopener noreferrer nofollow\"><u>source code<\/u><\/a><\/p>\n<h3>Login flow<\/h3>\n<p>Before logging in the user, we check if the provided email exists in our database. As mentioned before, the\u00a0<code>score<\/code>\u00a0is the user\u00a0<code>ID<\/code>.<\/p>\n<pre><code>const userId = await redis.zscore('emails', email);<\/code><\/pre>\n<p>If so, we first check if the email is verified by making sure the\u00a0<code>ID<\/code>\u00a0is a positive number. If not, we ask users to verify their email.<\/p>\n<p>If the email is verified, we get the password hash that we stored for the user:<\/p>\n<pre><code>const hash = await redis.hget('user:7', 'hash');<\/code><\/pre>\n<p>and check whether the password is correct:<\/p>\n<pre><code>const match = await bcrypt.compare(password, hash);<\/code><\/pre>\n<p>If the password is correct, we generate a token and return it to the client.<\/p>\n<p>And we are done.<\/p>\n<p>Login function\u00a0<a href=\"https:\/\/github.com\/sandorTuranszky\/questions-and-answers-board-built-with-redis\/blob\/main\/lambda\/login.js\" rel=\"noopener noreferrer nofollow\"><u>source code<\/u><\/a><\/p>\n<h3>Conclusion<\/h3>\n<p>As you can see, we needed four Redis commands for registration and only two for login.<\/p>\n<p>Probably you noticed that while describing the registration and login process with Redis we also revealed two more use cases for Redis, namely counter and rate-limiting.<\/p>\n<p>Redis has a lot more\u00a0<a href=\"https:\/\/redislabs.com\/redis-best-practices\/introduction\/\" rel=\"noopener noreferrer nofollow\"><u>use cases<\/u><\/a>\u00a0beyond cache and learning about them will only make you even more efficient.<\/p>\n<p>Follow me to read about how I am implementing a secure production-ready registration flow with email verification and password recovery backed by Redis.<\/p>\n<hr\/>\n<p>Check out my article on how I implemented the\u00a0<a href=\"https:\/\/habr.com\/en\/post\/568872\/\" rel=\"noopener noreferrer nofollow\"><u>LinkedIn-like reactions with Serverless Redis<\/u><\/a>.<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/569178\/\"> https:\/\/habr.com\/ru\/articles\/569178\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-394127","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/394127","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=394127"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/394127\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=394127"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=394127"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=394127"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}