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á . Vamos criar essa pasta e executar para gerar básico user-flow-boilerplate npm init -y package.json { "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 e colocar nosso arquivo de ponto de entrada inicial, , dentro dela. src index.ts 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 , , , , , . Todos esses arquivos que descrevi no seguinte artigo: . typscript lint jest bable prettier nodemon 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 deverá iniciar o servidor e você deverá ver uma saída semelhante a esta: npm run dev ./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é onde estabeleceremos uma coleção dedicada a testar nossos endpoints. Na nova coleção, adicione uma nova solicitação , pressione (no Mac, mas as chaves dependem do seu sistema operacional) e nomeie-a como . Carteiro GET cmd + E health Adicione entrada para URL: . Para , adicione uma nova variável que você usará em toda a coleção: {{BASE_URI}}/health BASE_URI http://localhost:9999/api/v1 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 . Para acessar e gerenciar o banco de dados, você pode utilizar várias plataformas de desenvolvimento como . docker-compose pgAdmin Pessoalmente, prefiro usar , que vem equipado com um driver que permite conectividade perfeita com bancos de dados PostgreSQL para gerenciamento eficiente. RubyMine Precisamos do arquivo com as chaves, senhas e nomes de teste necessários: .env 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="test_email@test.com" 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 para ilustrá-lo de uma maneira mais autêntica. Então, vamos criar um arquivo na raiz do projeto: JWT_SECRET docker-compose.yml 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 e temos que ser capazes de ver a saída após seguinte saída: docker-compose up docker ps 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 onde armazenamos nossos tipos para aplicação: src/@types/index.ts 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 na raiz do projeto e na pasta do banco de dados para conexão, migrações e sementes. knexfile.ts Deixei uma explicação bastante detalhada no artigo sobre como migrar e propagar usuários para o banco de dados onde estamos usando essas variáveis env. Criando um servidor Node.js com Postgres e Knex no Express 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 . Ok, se a conexão não tiver problemas, então poderemos migrar nossas tabelas para lá. Execute . Então você deve observar as colunas recém-adicionadas na tabela do banco de dados. psql knex migrate:latest users Vamos semeá-lo com dados falsos e verificar a tabela novamente. knex seed:run 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 , adicione o seguinte código: src/routes/index.ts 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 que já verificamos. Então, vamos atualizar o ponto de entrada para aplicar essas rotas lá. Primeiro, remova anterior. /health get -> 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 com código: health src/controllers/healthController.ts 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: e authRouter.ts 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. 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 , incluirei o modelo básico aqui para melhor visibilidade e compreensão. Crie em artigo 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 na mesma pasta: UserModel.ts 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 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. role 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 e adicione lá um arquivo com o seguinte código: src/middlewares authMiddleware.ts 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 ( ) e chama a função 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. req.user next() Tokens da Web JSON Vamos revisar o utilitário do jwt. Crie no arquivo com o seguinte código: src/utils/jwt.ts 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 exporta funções para assinar e verificar JWTs, aproveitando a biblioteca . 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. jwt jsonwebtoken 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 com o seguinte código: src/redis/index.ts 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 que deve estar em com o seguinte código: sessionController.ts src/controllers 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: 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. Informações da sessão do usuário: Ao verificar se 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. Autenticação e Autorização: req.user o controlador consulta o banco de dados (usando ) 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: Recuperação de perfil de usuário: UserModel 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 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. CACHE_EXPIRATION Antes de prosseguir com a criação do , que gerencia o processo de inscrição de novos usuários em nosso aplicativo, vamos revisar o esquema: signUpController 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 e adicionar o seguinte código: src/controllers/auth/signUpController.ts 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 predefinido para garantir que atendam ao formato necessário. userSchema Se a validação for bem-sucedida, indicando nenhum usuário existente e campos válidos, o controlador procederá ao hash da senha usando , gerará um e criará o usuário usando . bcrypt.hash username UserModel.create Por fim, ele gera um usando , define os dados no e envia o de volta ao usuário. token jwt session Redis token 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 , semelhante ao comportamento no . Invalid email or password 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 , devemos responder com um código de status . authorization 401 const authHeader = req.headers['authorization']; Crie o arquivo com o seguinte código: src/controllers/user/logoutController.ts 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 , é 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 . Se a operação for bem-sucedida, ela responderá com um código de status para indicar logout bem-sucedido. logoutController user.id 200 No entanto, se ocorrer um erro durante o processo, ele responde com um código de status para sinalizar um erro interno do servidor. 500 A seguir, vamos abordar a exclusão de dados do usuário. Crie e adicione este código: src/controllers/user/deleteUserController.ts 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 do Redis usando o cliente Redis. Posteriormente, invoca o método do para remover os dados do usuário do banco de dados. user_id delete UserModel Após a exclusão bem-sucedida da sessão e dos dados do usuário, ele responde com um código de status 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 para indicar um erro interno do servidor. 200 500 Para atualizar os dados do usuário no sistema, crie e adicione o seguinte código ao arquivo: src/controllers/user/updateUserController.ts 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 , e do corpo da solicitação. Em seguida, ele filtra esses campos usando a função de utilitário para garantir que apenas campos válidos sejam incluídos na carga útil. first_name last_name username filterObject Posteriormente, verifica se o fornecido já existe no banco de dados. Se isso acontecer, o controlador responderá com um código de status e uma mensagem de erro indicando um inválido. Se o for exclusivo, o controlador atualiza os dados do usuário no banco de dados usando o método do . username 400 username username updateOneById UserModel Após a atualização bem-sucedida, ele responde com um código de status 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 para indicar um erro interno do servidor. 200 500 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 e adicione o código: src/controllers/user/updatePasswordController.ts 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 , indicando uma solicitação incorreta. Em seguida, ele faz o hash da nova senha usando a biblioteca com um fator salt de 10. 400 bcrypt A senha com hash é então armazenada com segurança no banco de dados usando o método do , associando-a ao . Após a atualização bem-sucedida da senha, o controlador responde com um código de status e um objeto JSON contendo o ID do usuário. updateOneById UserModel user.id 200 Caso ocorra algum erro durante o processo de atualização da senha, o controlador responde com um código de status para indicar um erro interno do servidor como em outros controladores. 500 Certifique-se de revisar e configurar o auxiliar de validação e utilitários do . Depois de configurado, você deverá estar pronto para testar os endpoints. Repositório GitHub Vamos verificar o endpoint de inscrição: Como é evidente, obtivemos um token, que será utilizado no cabeçalho para recuperar a sessão. 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