paint-brush
Como dominar a autenticação e o fluxo do usuário em Node.js com Knex e Redisby@antonkalik
585
585

Como dominar a autenticação e o fluxo do usuário em Node.js com Knex e Redis

Anton Kalik26m2024/03/17
Read on Terminal Reader

Usando Knex para gerenciamento de banco de dados, Redis para cache eficiente e Express para roteamento contínuo. Crie e entenda uma solução de autenticação robusta para servidor Node js usando Knex e Redis. Use Knex para criar um banco de dados para Node.js, depois Redis para armazenar em cache e Express para rotear dados.
featured image - Como dominar a autenticação e o fluxo do usuário em Node.js com Knex e Redis
Anton Kalik HackerNoon profile picture
0-item

Crie uma solução de autenticação robusta para servidor Node js usando Knex para gerenciamento de banco de dados, Redis para cache eficiente e Express para roteamento contínuo.

Na minha busca por uma solução de autenticação rápida, intuitiva e simplificada para meus aplicativos Node.js, encontrei cenários que exigiam implementação rápida sem comprometer a funcionalidade.


Desde a inscrição e login do usuário até o gerenciamento de senhas esquecidas, atualização de dados do usuário e até mesmo exclusão de conta, procurei uma solução abrangente que navegasse perfeitamente por essas interações essenciais do usuário.


Assim, meu artigo pretende apresentar exatamente isso — uma abordagem coesa integrando metodologias claras para implementar autenticação e cache, garantindo um fluxo de usuários robusto e eficiente.


Aqui, ignoraremos os procedimentos fundamentais de instalação e criação de modelo, nos concentrando diretamente nas complexidades da autenticação e do fluxo do usuário. Incluíremos todos os links necessários para obter arquivos de configuração ao longo do artigo, garantindo acesso contínuo aos recursos necessários para a configuração.

Ferramentas

Para esta implementação, aproveitaremos a versão 20.11.1 do Node.js junto com Knex, Express e Redis. Além disso, utilizaremos PostgreSQL como nosso banco de dados, que será conteinerizado e orquestrado usando Docker para gerenciamento contínuo.


O nome do nosso aplicativo será user-flow-boilerplate . Vamos criar essa pasta e executar npm init -y para gerar package.json básico


 { "name": "user-flow-boilerplate", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

Pacote inicial.json


O próximo passo é adicionar as dependências necessárias:


dependências : npm i -S bcrypt body-parser cors dotenv express jsonwebtoken knex pg redis validator


devDependências :

 npm i -D @babel/core @babel/eslint-parser @babel/plugin-transform-class-properties @babel/plugin-transform-runtime @babel/preset-env @babel/preset-typescript @faker-js/faker @types/bcrypt @types/body-parser @types/cors @types/express @types/jest @types/jsonwebtoken @types/node @types/node-cron @types/validator @typescript-eslint/eslint-plugin @typescript-eslint/parser babel-jest cross-env eslint eslint-config-prettier eslint-plugin-prettier jest nodemon npm-run-all prettier ts-jest ts-loader ts-node tsconfig-paths tslint typescript webpack webpack-cli webpack-node-externals


e adicione scripts que irão construir e executar nosso aplicativo:

 "scripts": { "start": "NODE_ENV=production node dist/bundle.js", "build": "NODE_ENV=production webpack --config webpack.config.js", "dev": "cross-env NODE_ENV=development && npm-run-all -p dev:*", "dev:build": "webpack --config webpack.config.js --watch", "dev:start": "nodemon --watch dist --exec node dist/bundle.js", "test": "NODE_ENV=test jest --config ./jest.config.js", "lint": "eslint ./src -c .eslintrc.json" },


Para garantir o lançamento tranquilo de nosso aplicativo, é essencial criar uma pasta src e colocar nosso arquivo de ponto de entrada inicial, index.ts , dentro dela.

 require('dotenv').config(); import process from 'process'; import express from 'express'; import bodyParser from 'body-parser'; import cors from 'cors'; const app = express(); const PORT = process.env.PORT || 9999; app.use(bodyParser.json()); app.use(cors()); app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' })); (async () => { try { app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();

Arquivo de ponto de entrada


Para o desenvolvimento, precisamos ter configurações para typscript , lint , jest , bable , prettier , nodemon . Todos esses arquivos que descrevi no seguinte artigo: Criando um servidor Node.js com Postgres e Knex no Express .


Depois de definir todas as configurações e criar o ponto de entrada, a execução de npm run dev deverá iniciar o servidor e você deverá ver uma saída semelhante a esta:

 ./src/index.ts 1.7 KiB [built] [code generated] external "dotenv" 42 bytes [built] [code generated] external "process" 42 bytes [built] [code generated] external "express" 42 bytes [built] [code generated] external "body-parser" 42 bytes [built] [code generated] external "cors" 42 bytes [built] [code generated] webpack 5.90.3 compiled successfully in 751 ms [nodemon] restarting due to changes... [nodemon] starting `node dist/bundle.js` Server is running on port 9999


A seguir, navegue até Carteiro onde estabeleceremos uma coleção dedicada a testar nossos endpoints. Na nova coleção, adicione uma nova solicitação GET , pressione cmd + E (no Mac, mas as chaves dependem do seu sistema operacional) e nomeie-a como health .


Adicione entrada para URL: {{BASE_URI}}/health . Para BASE_URI , adicione uma nova variável que você usará em toda a coleção: http://localhost:9999/api/v1

URL base do conjunto de carteiro

Depois, basta clicar no botão ‘Enviar’ e você deverá observar o corpo da resposta:

 { "message": "OK" }


Base de dados

Antes de prosseguir, é crucial ter nosso banco de dados instalado e funcionando. Faremos isso lançando-o com docker-compose . Para acessar e gerenciar o banco de dados, você pode utilizar várias plataformas de desenvolvimento como pgAdmin .


Pessoalmente, prefiro usar RubyMine , que vem equipado com um driver que permite conectividade perfeita com bancos de dados PostgreSQL para gerenciamento eficiente.


Precisamos do arquivo .env com as chaves, senhas e nomes de teste necessários:

 PORT=9999 WEB_HOST="localhost" # DB DB_HOST="localhost" DB_PORT=5432 DB_NAME="user_flow_boilerplate" DB_USER="username_123" DB_PASSWORD="SomeParole999" # User DEFAULT_PASSWORD="SomeParole999" JWT_SECRET="6f1d7e9b9ba56476ae2f4bdebf667d88eeee6e6c98c68f392ed39f7cf6e51c5a" # Test User TEST_EMAIL="[email protected]" TEST_USERNAME="test_username" TEST_PASSWORD="SomeParole999" # Redis REDIS_HOST="localhost" REDIS_PORT=6379 REDIS_DB=0 REDIS_PASSWORD="SomeParole999"

.env para conexão com banco de dados, Redis e valores de teste para sementes


Não tenha medo, gerei aleatoriamente o JWT_SECRET para ilustrá-lo de uma maneira mais autêntica. Então, vamos criar um arquivo docker-compose.yml na raiz do projeto:

 version: '3.6' volumes: data: services: database: build: context: . dockerfile: postgres.dockerfile image: postgres:latest container_name: postgres environment: TZ: Europe/Madrid POSTGRES_DB: ${DB_NAME} POSTGRES_USER: ${DB_USER} POSTGRES_PASSWORD: ${DB_PASSWORD} networks: - default volumes: - data:/var/lib/postgresql/data ports: - "5432:5432" restart: unless-stopped redis: image: redis:latest container_name: redis command: redis-server --requirepass ${REDIS_PASSWORD} networks: - default ports: - "6379:6379" restart: unless-stopped

arquivo docker-compose com serviços


Vamos ativar dois serviços no Docker para conectividade rápida. Simplifiquei esse processo para facilitar o acesso rápido ao banco de dados ou Redis, permitindo-nos recuperar dados com eficiência. Então, vamos executar esses serviços docker-compose up e temos que ser capazes de ver a saída após docker ps seguinte saída:

 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES e4bef95de1dd postgres:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:5432->5432/tcp postgres 365e3a68351a redis:latest "docker-entrypoint.s…" About a minute ago Up About a minute 0.0.0.0:6379->6379/tcp redis


Agora, precisamos criar o arquivo src/@types/index.ts onde armazenamos nossos tipos para aplicação:

 export enum Role { Admin = 'admin', Blogger = 'blogger', } export type UserSession = { id: number; }; export type DatabaseDate = { created_at: Date; updated_at: Date; }; export type DefaultUserData = { role: Role; }; export interface User extends DatabaseDate { id: number; email: string; username: string; password: string; role: Role; }

Tipos de serviço


Neste momento, você precisa ter knexfile.ts na raiz do projeto e na pasta do banco de dados para conexão, migrações e sementes.


Deixei uma explicação bastante detalhada no artigo Criando um servidor Node.js com Postgres e Knex no Express sobre como migrar e propagar usuários para o banco de dados onde estamos usando essas variáveis env.


Gostaria de verificar especificamente as migrações para garantir que estamos na mesma página. Já lançamos nossos serviços e precisamos verificar a conexão com o banco de dados.

 docker exec -it postgres psql -U username_123 user_flow_boilerplate


Se a conexão estiver boa, você estará no console psql . Ok, se a conexão não tiver problemas, então poderemos migrar nossas tabelas para lá. Execute knex migrate:latest . Então você deve observar as colunas recém-adicionadas na tabela users do banco de dados.

Tabela de usuários após a migração

Vamos semeá-lo com dados falsos knex seed:run e verificar a tabela novamente.

Resultado no banco de dados após seed

Assim, agora estamos equipados para manipular o banco de dados, permitindo-nos adicionar, excluir ou atualizar usuários conforme necessário.

Roteador

Finalmente, podemos esquecer as configurações e a preparação e focar especificamente no fluxo do usuário. Para isso, precisamos criar um roteador. Precisamos lidar com esse roteador com as seguintes operações: login , logout , signup , delete_user , update_user .


Para isso, em src/routes/index.ts , adicione o seguinte código:

 import { Router } from 'express'; import { authRouter } from 'src/routes/authRouter'; import { healthController } from 'src/controllers/healthController'; import { sessionController } from 'src/controllers/sessionController'; import { authMiddleware } from 'src/middlewares/authMiddleware'; import { userRouter } from 'src/routes/userRouter'; export const router = Router({ mergeParams: true }); router.get('/health', healthController); router.use('/auth', authRouter); router.get('/session', authMiddleware, sessionController); router.use('/user', authMiddleware, userRouter); router.use((_, res) => { return res.status(404).json({ message: 'Not Found' }); });

Arquivo de rotas


Como você pode ver, no início adicionamos a rota /health que já verificamos. Então, vamos atualizar o ponto de entrada para aplicar essas rotas lá. Primeiro, remova get anterior.

 -> REMOVE -> app.get('/api/v1/health', (req, res) => res.status(200).json({ message: 'OK' }));


e adicione no topo do arquivo:

 import { router } from 'src/routes'; // ... app.use(cors()); app.use('/api/v1', router);


e crie o primeiro controlador para verificação health src/controllers/healthController.ts com código:

 import { Request, Response } from 'express'; export const healthController = (_: Request, res: Response) => res.status(200).send('ok');

Controlador de saúde


Agora, vamos voltar ao roteador e verificar o que temos para adicionar mais às rotas. Precisamos adicionar mais dois arquivos: authRouter.ts e userRouter.ts

 import { Router } from 'express'; import { signUpController } from 'src/controllers/auth/signUpController'; import { loginController } from 'src/controllers/auth/loginController'; export const authRouter = Router(); authRouter.post('/signup', signUpController); authRouter.post('/login', loginController);

Roteador de autenticação


 import { Router } from 'express'; import { updateUserController } from 'src/controllers/user/updateUserController'; import { deleteUserController } from 'src/controllers/user/deleteUserController'; import { logoutController } from 'src/controllers/user/logoutController'; import { updatePasswordController } from 'src/controllers/user/updatePasswordController'; export const userRouter = Router(); userRouter.patch('/', updateUserController); userRouter.delete('/', deleteUserController); userRouter.post('/logout', logoutController); userRouter.post('/update-password', updatePasswordController);

Roteador do usuário


Dividi essa lógica por uma questão de legibilidade e responsabilidade de manter a funcionalidade isolada. Todas essas rotas precisam de controladores onde iremos lidar com a lógica.


Controladores


As rotas de autenticação e integridade não precisam de middleware de autenticação, portanto, essas rotas não são protegidas, mas se não houver correspondência, obteremos o status 404.

 router.get('/health', healthController); router.use('/auth', authRouter);

Agora que já definimos todas as rotas, temos que definir o modelo do usuário.

Modelo de usuário

Utilizarei um modelo base para o modelo de usuário, a partir do qual reutilizarei métodos CRUD. Embora eu já tenha abordado a criação de modelos em outro artigo , incluirei o modelo básico aqui para melhor visibilidade e compreensão. Crie em src/models/Model.ts

 import { database } from 'root/database'; export abstract class Model { protected static tableName?: string; protected static get table() { if (!this.tableName) { throw new Error('The table name must be defined for the model.'); } return database(this.tableName); } public static async insert<Payload>(data: Payload): Promise<{ id: number; }> { const [result] = await this.table.insert(data).returning('id'); return result; } public static async updateOneById<Payload>( id: number, data: Payload ): Promise<{ id: number; }> { const [result] = await this.table.where({ id }).update(data).returning('id'); return result; } public static async delete(id: number): Promise<number> { return this.table.where({ id }).del(); } public static async findOneById<Result>(id: number): Promise<Result> { return this.table.where('id', id).first(); } public static async findOneBy<Payload, Result>(data: Payload): Promise<Result> { return this.table.where(data as string).first(); } }

Modelo básico


Com o modelo base, precisamos criar UserModel.ts na mesma pasta:

 import { Model } from 'src/models/Model'; import { Role, User, DefaultUserData } from 'src/@types'; export class UserModel extends Model { static tableName = 'users'; public static async create<Payload>(data: Payload) { return super.insert<Payload & DefaultUserData>({ ...data, role: data.role || Role.Blogger, }); } public static findByEmail(email: string): Promise<User | null> { return this.findOneBy< { email: string; }, User >({ email }); } public static findByUsername(username: string): Promise<User | null> { return this.findOneBy< { username: string; }, User >({ username }); } }

Modelo de usuário


No modelo de usuário, defino role apenas por padrão, se não for fornecida pela carga útil. E agora que temos nossos modelos prontos, podemos utilizá-los em nossos controladores e middlewares.

Middleware de autenticação

O middleware de autenticação em uma aplicação Node.js é responsável por autenticar as solicitações recebidas, garantindo que elas sejam provenientes de usuários válidos e autorizados.


Normalmente, ele intercepta solicitações recebidas, extrai tokens ou credenciais de autenticação e verifica sua validade em relação a um mecanismo de autenticação predefinido, como JWT (JSON Web Tokens), neste caso.


Se o processo de autenticação for bem-sucedido, o middleware permitirá que a solicitação prossiga para o próximo manipulador no ciclo solicitação-resposta. Entretanto, se a autenticação falhar, ele responde com um código de status HTTP apropriado (por exemplo, 401 Não Autorizado) e, opcionalmente, fornece uma mensagem de erro.


Crie a pasta src/middlewares e adicione lá um arquivo authMiddleware.ts com o seguinte código:

 import { jwt } from 'src/utils/jwt'; import { Redis } from 'src/redis'; import type { Request, Response, NextFunction } from 'express'; import type { UserSession } from 'src/@types'; export async function authMiddleware(req: Request, res: Response, next: NextFunction) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; const JWT_SECRET = process.env.JWT_SECRET; if (!token) return res.sendStatus(401); if (!JWT_SECRET) { console.error('JWT_SECRET Not Found'); return res.sendStatus(500); } if (!token) return res.status(401).json({ error: 'Token not provided' }); try { const userSession = await jwt.verify<UserSession>(token); if (!userSession) { return res.sendStatus(401); } const storedToken = await Redis.getSession(userSession.id); if (!storedToken || storedToken !== token) { return res.sendStatus(401); } req.user = userSession; next(); } catch (error) { console.error('JWT_ERROR', error); return res.sendStatus(401); } }

Arquivo de middleware de autenticação


O middleware de autenticação extrai o token JWT do cabeçalho da solicitação, verifica sua validade usando a biblioteca JWT e verifica se o token corresponde ao armazenado no Redis.


Se o token for válido e corresponder ao token armazenado, o middleware define a sessão do usuário autenticado no objeto de solicitação ( req.user ) e chama a função next() para passar o controle para o próximo middleware ou manipulador de rota. Caso contrário, ele responde com um código de status 401 indicando falha na autenticação.

Tokens da Web JSON

Vamos revisar o utilitário do jwt. Crie no arquivo src/utils/jwt.ts com o seguinte código:

 require('dotenv').config(); import jsonwebtoken from 'jsonwebtoken'; const JWT_SECRET = process.env.JWT_SECRET as string; export const jwt = { verify: <Result>(token: string): Promise<Result> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { jsonwebtoken.verify(token, JWT_SECRET, (error, decoded) => { if (error) { reject(error); } else { resolve(decoded as Result); } }); }); }, sign: (payload: string | object | Buffer): Promise<string> => { if (!JWT_SECRET) { throw new Error('JWT_SECRET not found in environment variables!'); } return new Promise((resolve, reject) => { try { resolve(jsonwebtoken.sign(payload, JWT_SECRET)); } catch (error) { reject(error); } }); }, };

Arquivo utilitário JWT


Este utilitário desempenha um papel crítico no tratamento de JSON Web Tokens no aplicativo Node.js. O objeto jwt exporta funções para assinar e verificar JWTs, aproveitando a biblioteca jsonwebtoken . Estas funções facilitam a criação e validação de JWTs, essenciais para a implementação de mecanismos de autenticação na aplicação.


O utilitário encapsula a funcionalidade para lidar com JWTs, garantindo mecanismos de autenticação seguros dentro do aplicativo Node.js, ao mesmo tempo que segue as melhores práticas para gerenciamento de variáveis de ambiente.

Redis

Usado como banco de dados, cache e corretor de mensagens. Geralmente usado em diversos casos de uso, incluindo cache, gerenciamento de sessões, análises em tempo real, filas de mensagens, placares e muito mais.


A verificação do token do Redis serve como uma camada adicional de segurança e validação para o token JWT. Vamos mergulhar nas configurações. Para isso, crie no arquivo src/redis/index.ts com o seguinte código:

 require('dotenv').config({ path: '../../.env', }); import process from 'process'; import * as redis from 'redis'; const client = redis.createClient({ url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`, }); client.on('error', error => console.error('Redis Client Error', error)); const connect = async () => { try { await client.connect(); console.log('Connected to Redis'); } catch (err) { console.error(`Could not connect to Redis: ${err}`); process.exit(1); } }; class Redis { public static setSession(userId: number, token: string) { if (!userId) throw new Error('userId is required'); if (!token) throw new Error('token is required'); try { return client.set(`session:${userId}`, token); } catch (error) { console.error(error); } } public static getSession(userId: number) { if (!userId) throw new Error('userId is required'); return client.get(`session:${userId}`); } public static deleteSession(userId: string) { if (!userId) throw new Error('userId is required'); try { return client.del(`session:${userId}`); } catch (error) { console.error(error); } } } export { client, connect, Redis };

Loja de sessões Redis


Pelo Redis, iremos armazenar e gerenciar tokens de sessão do usuário. No middleware de autenticação, após verificar a autenticidade do token JWT, o middleware verifica se o token existe e corresponde ao armazenado no Redis para a sessão do usuário correspondente. Isto ajuda a garantir que apenas usuários válidos e autorizados possam acessar rotas protegidas.


Redis é usado como um armazenamento de valor-chave para manter tokens de sessão do usuário. Quando um usuário faz login ou se autentica, seu token de sessão é armazenado no Redis. Isso permite a recuperação rápida e eficiente de tokens de sessão durante verificações de autenticação subsequentes.


O Redis é utilizado no middleware de autenticação para gerenciamento eficiente de sessões, enquanto o arquivo relacionado ao Redis cuida da configuração e conexão com o servidor Redis e fornece funções para interagir com o Redis em outras partes do aplicativo.


Esta configuração garante mecanismos de autenticação seguros e confiáveis, com tokens de sessão de usuário armazenados e gerenciados no Redis.


A última parte é que precisamos nos conectar ao Redis em nosso ponto de entrada:

 // all imports import * as Redis from 'src/redis'; const app = express(); const PORT = process.env.PORT || 9999; // middlewares (async () => { try { await Redis.connect(); app.listen(PORT, async () => { console.log(`Server is running on port ${PORT}`); }); } catch (error) { console.error('Failed to start server:', error); process.exit(1); } })();

Conecte-se ao Redis


Após concluir a preparação da autenticação, podemos agora mudar nosso foco para os controladores.

Controladores

Os controladores nas rotas ajudam a organizar a lógica da aplicação, separando preocupações e promovendo a manutenção do código. Já criamos o controlador para a verificação de integridade. A seguir, prosseguiremos com a criação de controladores para lidar com operações com o usuário.


O primeiro controlador que vamos usar é o sessionController.ts que deve estar em src/controllers com o seguinte código:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const user = await UserModel.findOneById<User>(req.user.id); if (user) { return res.status(200).json(user); } else { return res.sendStatus(401); } } catch (error) { return res.sendStatus(500); } };

Controlador de sessão


Este controlador serve ao propósito de lidar com um endpoint relacionado à sessão, provavelmente responsável por recuperar informações sobre o usuário atualmente autenticado. Precisamos deste controlador pelos seguintes motivos:


Informações da sessão do usuário: Este controlador permite que o aplicativo recupere informações sobre a sessão do usuário, como perfil de usuário ou outros dados relevantes. Essas informações podem ser úteis para personalizar a experiência do usuário ou fornecer conteúdo personalizado com base no perfil do usuário.


Autenticação e Autorização: Ao verificar se req.user existe, o controlador garante que apenas usuários autenticados possam acessar o endpoint. Isso ajuda a impor regras de autenticação e autorização, garantindo que os dados confidenciais do usuário sejam acessíveis apenas a usuários autorizados.


Recuperação de perfil de usuário: o controlador consulta o banco de dados (usando UserModel ) para recuperar as informações do usuário com base em seu ID de sessão. Isso permite que o aplicativo busque dados específicos do usuário de forma dinâmica, proporcionando uma experiência personalizada para cada usuário. Esta parte definitivamente pode ser melhorada pelo cache Redis:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; export const sessionController = async (req: Request, res: Response) => { if (!req.user) return res.sendStatus(401); try { const cachedProfile = await Redis.getSession(req.user.id); if (cachedProfile) { return res.status(200).json(JSON.parse(cachedProfile)); } else { const user = await UserModel.findOneById<User>(req.user.id); if (user) { await Redis.setSession(req.user.id, JSON.stringify(user), CACHE_EXPIRATION); return res.status(200).json(user); } else { return res.sendStatus(401); } } } catch (error) { console.error('Error retrieving user profile:', error); return res.sendStatus(500); } };

Arquivo do controlador de sessão com sessão definida do Redis


Definimos uma constante CACHE_EXPIRATION para especificar o tempo de expiração do cache em segundos. Neste exemplo, está definido para 3.600 segundos (1 hora). Os dados armazenados em cache são atualizados periodicamente, evitando que dados obsoletos sejam fornecidos aos usuários e mantendo a integridade dos dados dentro do cache.


Antes de prosseguir com a criação do signUpController , que gerencia o processo de inscrição de novos usuários em nosso aplicativo, vamos revisar o esquema:

Esquema do processo de inscrição

No nosso caso, ao tentar se cadastrar com um e-mail existente no banco de dados, priorizamos a privacidade do usuário, não revelando explicitamente se o usuário existe. Em vez disso, informamos o cliente com uma mensagem genérica informando Invalid email or password .


Essa abordagem incentiva o cliente a enviar credenciais válidas sem divulgar informações desnecessárias sobre os usuários existentes.


Agora vamos criar src/controllers/auth/signUpController.ts e adicionar o seguinte código:

 import bcrypt from 'bcrypt'; import { jwt } from 'src/utils/jwt'; import { Request, Response } from 'express'; import { validate } from 'src/helpers/validation/validate'; import { userSchema } from 'src/helpers/validation/schemas/userSchema'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; import type { User } from 'src/@types'; import { getRandomString } from 'src/utils/getRandomString'; type Payload = Omit<User, 'id' | 'created_at' | 'updated_at' | 'role'>; export async function signUpController(req: Request, res: Response) { const { email, password }: Payload = req.body; const validation = validate<Payload>(req.body, userSchema); if (!validation.isValid) { return res.status(400).send(`Invalid ${validation.invalidKey}`); } try { const user = await UserModel.findOneBy({ email }); if (user) { return res.status(400).json({ message: 'Invalid email or password' }); } const hashedPassword = (await bcrypt.hash(password, 10)) as string; const username = `${email.split('@')[0]}${getRandomString(5)}`; const createdUser = await UserModel.create<Payload>({ email, password: hashedPassword, username, }); const token = await jwt.sign({ id: createdUser.id, }); await Redis.setSession(createdUser.id, token); res.status(200).json({ token, }); } catch (error) { return res.sendStatus(500); } }

Controlador de inscrição


O controlador recebe uma solicitação contendo o e-mail e a senha do usuário, normalmente a partir de um formulário de inscrição. Ele valida os dados recebidos em relação a um userSchema predefinido para garantir que atendam ao formato necessário.


Se a validação for bem-sucedida, indicando nenhum usuário existente e campos válidos, o controlador procederá ao hash da senha usando bcrypt.hash , gerará um username e criará o usuário usando UserModel.create .


Por fim, ele gera um token usando jwt , define os dados session no Redis e envia o token de volta ao usuário.


Agora, vamos nos concentrar na criação de um controlador de login. Crie o arquivo src/controllers/auth/loginController.ts :


 require('dotenv').config({ path: '../../.env', }); import bcrypt from 'bcrypt'; import { Request, Response } from 'express'; import { jwt } from 'src/utils/jwt'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export async function loginController(req: Request, res: Response) { const { email, password } = req.body; if (!email || !password) { return res.status(400).json({ message: 'Invalid email or password' }); } try { const user = await UserModel.findByEmail(email); if (user) { const isValidPassword = await bcrypt.compare(password, user.password); if (!isValidPassword) { return res.status(400).json({ message: 'Invalid email or password' }); } const token: string = await jwt.sign({ id: user.id, }); await Redis.setSession(user.id, token); res.status(200).json({ token }); } else { return res.status(400).json({ message: 'Invalid email or password' }); } } catch (error) { console.error(error); return res.sendStatus(500); } }

Controlador de login


Essencialmente, começamos validando os campos fornecidos e depois verificando a existência de um usuário. Se nenhum usuário for encontrado, respondemos com um código de status 400 junto com a mensagem Invalid email or password , semelhante ao comportamento no signupController .


Se existir um usuário, comparamos a senha fornecida com a senha com hash armazenada no banco de dados usando bcrypt.compare .


Se as senhas não corresponderem, responderemos com a mensagem familiar 'E-mail ou senha inválidos'. Finalmente, após a autenticação bem-sucedida, geramos um token, configuramos a sessão no Redis e enviamos o token de volta ao cliente.


Vamos revisar nossos controladores protegidos, que dependem da presença de um user_id obtido do middleware. Confiamos consistentemente neste user_id para operações dentro desses controladores. Nos casos em que a solicitação não possui cabeçalho authorization , devemos responder com um código de status 401 .


 const authHeader = req.headers['authorization'];


Crie o arquivo src/controllers/user/logoutController.ts com o seguinte código:


 import type { Request, Response } from 'express'; import { Redis } from 'src/redis'; export async function logoutController(req: Request, res: Response) { try { await Redis.deleteSession(req.user.id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } }

Controlador de logout


Este logoutController , é responsável por desconectar um usuário do sistema. Ao receber uma solicitação, ele interage com o cliente Redis para excluir a sessão associada ao user.id . Se a operação for bem-sucedida, ela responderá com um código de status 200 para indicar logout bem-sucedido.


No entanto, se ocorrer um erro durante o processo, ele responde com um código de status 500 para sinalizar um erro interno do servidor.


A seguir, vamos abordar a exclusão de dados do usuário.


Crie src/controllers/user/deleteUserController.ts e adicione este código:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { Redis } from 'src/redis'; export const deleteUserController = async (req: Request, res: Response) => { const user_id = req.user.id; try { await Redis.deleteSession(user_id); await UserModel.delete(user_id); return res.sendStatus(200); } catch (error) { return res.sendStatus(500); } };

Excluir controlador de usuário


Quando uma solicitação é recebida, ele extrai o ID do usuário do objeto de solicitação, normalmente obtido do middleware de autenticação.


Posteriormente, ele exclui a sessão associada a este user_id do Redis usando o cliente Redis. Posteriormente, invoca o método delete do UserModel para remover os dados do usuário do banco de dados.


Após a exclusão bem-sucedida da sessão e dos dados do usuário, ele responde com um código de status 200 para indicar a exclusão bem-sucedida. No caso de um erro durante o processo de exclusão, ele responde com um código de status 500 para indicar um erro interno do servidor.


Para atualizar os dados do usuário no sistema, crie src/controllers/user/updateUserController.ts e adicione o seguinte código ao arquivo:

 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import { filterObject } from 'src/utils/filterObject'; type Payload = { first_name?: string; last_name?: string; username?: string; }; export const updateUserController = async (req: Request, res: Response) => { const { first_name, last_name, username } = req.body; const payload: Payload = filterObject({ first_name, last_name, username, }); try { const existingUserName = await UserModel.findByUsername(username); if (existingUserName) { return res.status(400).json({ error: 'Invalid username', }); } const updatedUser = await UserModel.updateOneById<typeof payload>(req.user.id, payload); res.status(200).json(updatedUser); } catch (error) { res.sendStatus(500); } };

Atualizar controlador de usuário


Ao receber uma solicitação, ele extrai os campos first_name , last_name e username do corpo da solicitação. Em seguida, ele filtra esses campos usando a função de utilitário filterObject para garantir que apenas campos válidos sejam incluídos na carga útil.


Posteriormente, verifica se o username fornecido já existe no banco de dados. Se isso acontecer, o controlador responderá com um código de status 400 e uma mensagem de erro indicando um username inválido. Se o username for exclusivo, o controlador atualiza os dados do usuário no banco de dados usando o método updateOneById do UserModel .


Após a atualização bem-sucedida, ele responde com um código de status 200 e os dados do usuário atualizados. Em caso de erros durante o processo de atualização, o controlador responde com um código de status 500 para indicar um erro interno do servidor.


A última será atualizar a senha, praticamente a mesma ideia da atualização dos dados do usuário, mas com hash da nova senha. Crie o último controlador da nossa lista src/controllers/user/updatePasswordController.ts e adicione o código:


 import { Request, Response } from 'express'; import { UserModel } from 'src/models/UserModel'; import bcrypt from 'bcrypt'; export const updatePasswordController = async (req: Request, res: Response) => { try { const { password } = req.body; if (!password) return res.sendStatus(400); const hashedPassword = (await bcrypt.hash(password, 10)) as string; const user = await UserModel.updateOneById(req.user.id, { password: hashedPassword }); return res.status(200).json({ id: user.id }); } catch (error) { return res.sendStatus(500); } };

Atualizar controlador de senha


Ao receber uma solicitação, extrai a nova senha do corpo da solicitação. Em seguida, verifica se uma senha é fornecida no corpo da solicitação. Caso contrário, ele responde com um código de status 400 , indicando uma solicitação incorreta. Em seguida, ele faz o hash da nova senha usando a biblioteca bcrypt com um fator salt de 10.


A senha com hash é então armazenada com segurança no banco de dados usando o método updateOneById do UserModel , associando-a ao user.id . Após a atualização bem-sucedida da senha, o controlador responde com um código de status 200 e um objeto JSON contendo o ID do usuário.


Caso ocorra algum erro durante o processo de atualização da senha, o controlador responde com um código de status 500 para indicar um erro interno do servidor como em outros controladores.


Certifique-se de revisar e configurar o auxiliar de validação e utilitários do Repositório GitHub . Depois de configurado, você deverá estar pronto para testar os endpoints.


Vamos verificar o endpoint de inscrição:


Ponto de inscrição de autenticação


Como é evidente, obtivemos um token, que será utilizado no cabeçalho para recuperar a sessão.


Resultado da sessão em resposta


Enviamos o token de autorização no cabeçalho para o servidor e, em resposta, o servidor nos forneceu os dados do usuário recuperados do banco de dados.


Sinta-se à vontade para explorar e experimentar recursos de segurança e cache Redis. Com o modelo básico implementado, você pode se aprofundar em funcionalidades adicionais, como recuperação de conta para usuários que esquecem suas senhas. No entanto, este tópico ficará reservado para um artigo futuro.

Conclusão

Gerenciar o roteamento e o fluxo de autenticação de usuários de maneira escalonável pode ser um desafio. Embora tenhamos implementado middleware para proteger rotas, existem estratégias adicionais disponíveis para melhorar o desempenho e a confiabilidade do serviço.


A experiência do usuário é ainda melhorada ao fornecer mensagens de erro mais claras, já que o tratamento de erros continua sendo um aspecto significativo que requer uma cobertura mais abrangente. No entanto, implementamos com sucesso o fluxo de autenticação primário, permitindo que os usuários se inscrevam, acessem suas contas, recuperem dados de sessão, atualizem informações do usuário e excluam contas.


Espero que você tenha achado esta jornada esclarecedora e adquirido conhecimento valioso sobre autenticação de usuários.

Recursos

Repositório GitHub
Knex.js
Expressar
Criar aplicativo de nó
Carteiro


Também publicado aqui