Há muitos casos de uso para bancos de dados NoSQL, e um que encontro com frequência é a criação de um armazenamento e uma sessão de perfil de usuário. Esse caso de uso se presta a um Banco de dados NoSQL. Os perfis geralmente precisam ser flexíveis e aceitar alterações nos dados. Embora isso seja possível em um RDBMS, seria necessário mais trabalho para manter os dados com penalidades de desempenho.
Kirk Kirkconnell escreveu um exemplo de alto nível para criar um armazenamento e uma sessão de perfil de usuário com o Couchbase: Armazenamento de perfil de usuário: Modelagem avançada de dados. Vamos expandir esses conceitos e implementar um armazenamento de perfil de usuário usando bancos de dados Node.js e Servidor Couchbase.
Este tutorial foi atualizado em 4 de janeiro de 2021 por Eric Bishard para trabalhar com o Couchbase NodeJS SDK 3!
Antes de escrevermos algum código, vamos descobrir o que estamos tentando realizar.
Quando se trata de gerenciar dados de usuários, precisamos de uma maneira de criar um armazenamento e uma sessão de perfil de usuário e associar outros documentos a eles. Vamos definir algumas regras em torno desse conceito de armazenamento de perfil de usuário:
- Armazene os dados da conta, como nome de usuário e senha, em um documento de perfil.
- Passe dados confidenciais do usuário com cada solicitação de ação do usuário.
- Use uma sessão que expira após um período de tempo definido.
- Documentos de sessão armazenados com um limite de expiração.
Podemos gerenciar tudo isso com os seguintes pontos de extremidade da API:
- POST /account - Cria um novo perfil de usuário com informações da conta
- POST /login - Validar informações da conta
- GET /account - Obter informações da conta
- POST /blog - Cria uma nova entrada de blog associada a um usuário
- GET /blogs - Obtém todas as entradas de blog de um determinado usuário
Esses pontos de extremidade farão parte de nosso backend de API que utiliza o SDK do Couchbase Server Node.js (este artigo foi atualizado para usar a versão 3.1.x).
Criação da API com o Node e o Express
Vamos criar um diretório de projeto para nosso aplicativo Node.js e instalar nossas dependências.
1 2 |
mkdir blog-API && cd blog-API && npm inicial -y npm instalar couchbase expresso corpo-analisador uuid bcryptjs cors --salvar |
Isso cria um diretório de trabalho para o nosso projeto e inicializa um novo projeto Node. Nossas dependências incluem o SDK do Node.js para o Couchbase e Express Framework e outras bibliotecas de utilitários, como analisador de corpo
para aceitar dados JSON por meio de solicitações POST, uuid
para gerar chaves exclusivas e bcryptjs
para fazer hash de nossas senhas e impedir usuários mal-intencionados.
Vamos inicializar nosso aplicativo com um servidor.js file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
const couchbase = exigir('couchbase') const expresso = exigir("expresso) const uuid = exigir('uuid') const bodyParser = exigir('body-parser') const bcrypt = exigir('bcryptjs') const cors = exigir('cors') const aplicativo = expresso() aplicativo.uso(cors()) aplicativo.uso(bodyParser.json()) aplicativo.uso(bodyParser.codificado por url({ estendido: verdadeiro })) const agrupamento = novo couchbase.Aglomerado('couchbase://localhost', { nome de usuário: "Administrador, senha: 'senha' }) const balde = agrupamento.balde('blog') const coleção = balde.defaultCollection() const servidor = aplicativo.ouvir(3000, () => console.informações(`Em execução em porto ${servidor.endereço().porto}...`)) |
O código acima requer nossas dependências e inicializa um aplicativo Express em execução na porta 3000 no Couchbase Server usando um bucket chamado blog
.
Também precisamos criar um índice no Couchbase Server porque usaremos a linguagem de consulta N1QL para um de nossos endpoints. Se acessarmos nosso console da Web do Couchbase Server em execução localmente em localhost:8091podemos clicar no botão Consulta e execute essa declaração no Query Editor:
1 |
CRIAR ÍNDICE `usuário do blog` ON `padrão`(tipo, pid); |
Como obteremos todas as publicações de blog para um determinado ID de perfil, teremos um desempenho melhor usando esse índice específico em vez de um índice primário geral. Os índices primários não são recomendados para códigos em nível de produção.
Como salvar um novo usuário no Profile Store
Sabemos que um perfil de usuário pode ter qualquer informação que descreva um usuário. Informações como endereço, telefone, informações de mídia social, etc. Nunca é uma boa ideia armazenar credenciais de conta no mesmo documento que nossas informações básicas de perfil. Precisaremos de, no mínimo, dois documentos para cada usuário. Vamos dar uma olhada em como esses documentos serão estruturados.
Nosso documento de perfil terá uma chave à qual faremos referência em nossos documentos relacionados. Essa chave é um UUID gerado automaticamente: b181551f-071a-4539-96a5-8a3fe8717faf
.
Nosso documento Profile terá um valor JSON que inclui duas propriedades: e-mail
e um tipo
propriedade. O tipo
A propriedade é um indicador importante que descreve nosso documento de forma semelhante a como uma tabela organiza os registros em um banco de dados relacional. Essa é uma convenção padrão em um banco de dados de documentos.
1 2 3 4 |
{ "tipo": "profile" (perfil), "email": "user1234@gmail.com" } |
O documento da conta associado ao nosso perfil terá uma chave que é igual ao e-mail do usuário:
user1234@gmail.com
e esse documento terá um tipo
bem como um pid
referindo-se à chave de nosso documento de perfil, juntamente com e-mail
e com hash senha
.
1 2 3 4 5 6 |
{ "tipo": "account" (conta), "pid": "b181551f-071a-4539-96a5-8a3fe8717faf", "email": "user1234@gmail.com", "senha": "$2a$10$tZ23pbQ1sCX4BknkDIN6NekNo1p/Xo.Vfsttm.USwWYbLAAspeWsC" } |
Ótimo, estabelecemos um modelo para cada documento e uma estratégia para relacionar esses documentos sem restrições de banco de dados.
Um ponto de extremidade para a criação de contas
Adicione o seguinte código ao nosso servidor.js
file:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
aplicativo.postagem("/account", assíncrono (solicitação, resposta) => { se (!solicitação.corpo.e-mail) { retorno resposta.status(401).enviar({ "mensagem": "É necessário um `email`" }) } mais se (!solicitação.corpo.senha) { retorno resposta.status(401).enviar({ "mensagem": "Uma `senha` é necessária" }) } const id = uuid.v4() const conta = { "tipo": "account" (conta), "pid": id, "email": solicitação.corpo.e-mail, "senha": bcrypt.hashSync(solicitação.corpo.senha, 10) } const perfil = { "tipo": "profile" (perfil), "email": solicitação.corpo.e-mail } aguardar coleção.inserir(id, perfil) .então(assíncrono () => { aguardar coleção.inserir(solicitação.corpo.e-mail, conta) .então((resultado) => { resultado.pid = id retorno resposta.enviar(resultado) }) .captura(assíncrono (e) => { aguardar coleção.remover(id) .então(() => { console.erro(`conta criação falhou, removido: ${id}`) retorno resposta.status(500).enviar(e) }) .captura(e => resposta.status(500).enviar(e)) }) }) .captura(e => resposta.status(500).enviar(e)) }) |
Vamos detalhar esse código.
Primeiro, verificamos se os dois an e-mail
e senha
existem na solicitação.
Em seguida, criamos um conta
objeto e perfil
com base nos dados que foram enviados na solicitação. O objeto pid
que estamos salvando no conta
é uma chave exclusiva. Ele será definido como a chave do documento para o nosso perfil
objeto.
O conta O documento usa o e-mail como chave. No futuro, se forem necessários outros detalhes da conta (como e-mail alternativo, login social etc.), poderemos associar outros documentos ao perfil.
Em vez de salvar a senha no arquivo conta
como texto simples, fazemos o hash com Bcrypt. A senha é removida do perfil
para segurança. Para obter mais informações sobre hashing de senha, consulte este tutorial.
Com os dados prontos, podemos inseri-los no Couchbase. O objetivo desse salvamento é ser tudo ou nada. Queremos que tanto o conta e perfil para que os documentos sejam criados com sucesso, caso contrário, reverteremos tudo. Dependendo do sucesso, retornaremos algumas informações para o cliente.
Poderíamos ter usado consultas N1QL para inserir os dados, mas é mais fácil usar operações CRUD sem prejudicar o desempenho.
Uso de tokens de sessão para dados confidenciais
Com o perfil de usuário e a conta criados, queremos que o usuário faça login e comece a realizar atividades que armazenem dados e os associem a ele.
Queremos fazer login e estabelecer uma sessão que será armazenada no banco de dados com referência ao nosso perfil de usuário. Esse documento acabará expirando e será removido do banco de dados.
O modelo de sessão será parecido com o seguinte:
1 2 3 4 5 |
{ "tipo": "sessão", "id": "ce0875cb-bd27-48eb-b561-beee33c9f405", "pid": "b181551f-071a-4539-96a5-8a3fe8717faf" } |
Este documento, assim como os outros, tem uma tipo
. Assim como no caso do conta documento, ele tem um pid
que faz referência a um perfil de usuário.
O código que torna isso possível está no login ponto final:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
aplicativo.postagem("/login", assíncrono (solicitação, resposta) => { se (!solicitação.corpo.e-mail) { retorno resposta.status(401).enviar({ "mensagem": "É necessário um `email`" }) } mais se (!solicitação.corpo.senha) { retorno resposta.status(401).enviar({ "mensagem": "Uma `senha` é necessária" }) } aguardar coleção.obter(solicitação.corpo.e-mail) .então(assíncrono (resultado) => { se (!bcrypt.compareSync(solicitação.corpo.senha, resultado.valor.senha)) { retorno resposta.status(500).enviar({ "mensagem": "Senha inválida" }) } var sessão = { "tipo": "sessão", "id": uuid.v4(), "pid": resultado.valor.pid } aguardar coleção.inserir(sessão.id, sessão, { "expiração": 3600 }) .então(() => resposta.enviar({ "sid": sessão.id })) .captura(e => resposta.status(500).enviar(e)) }) .captura(e => resposta.status(500).enviar(e)) }) |
Depois de validar os dados recebidos, fazemos uma pesquisa de conta por endereço de e-mail. Se os dados do e-mail forem retornados, poderemos comparar a senha recebida com a senha com hash retornada na pesquisa de conta. Se isso for bem-sucedido, poderemos criar uma nova sessão para o usuário.
Diferentemente da operação de inserção anterior, definimos uma expiração de documento de uma hora (3600 s). Se a expiração não for atualizada, o documento desaparecerá. Isso é bom porque força o usuário a fazer login novamente e obter uma nova sessão. Esse token de sessão será passado em todas as solicitações futuras em vez da senha.
Gerenciamento de uma sessão de usuário com tokens
Queremos obter informações sobre nosso perfil de usuário, bem como associar coisas novas ao perfil. Para isso, confirmamos a autoridade por meio da sessão.
Podemos confirmar que a sessão é válida usando middleware. Uma função de middleware pode ser adicionada a qualquer endpoint do Express. Essa validação é uma função simples que terá acesso à solicitação HTTP do nosso endpoint:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
const validar = assíncrono(solicitação, resposta, próxima) => { const authHeader = solicitação.cabeçalhos["autorização"] se (authHeader) { bearerToken = authHeader.dividir(" ") se (bearerToken.comprimento == 2) { aguardar coleção.obter(bearerToken[1]) .então(assíncrono(resultado) => { solicitação.pid = resultado.valor.pid aguardar coleção.toque(bearerToken[1], 3600) .então(() => próxima()) .captura((e) => console.erro(e.mensagem)) }) .captura((e) => resposta.status(401).enviar({ "mensagem": "Token de sessão inválido" })) } } mais { resposta.status(401).enviar({ "mensagem": "É necessário um cabeçalho de autorização" }) } } |
Aqui estamos verificando se há um cabeçalho de autorização na solicitação. Se tivermos um token de portador válido com o ID da sessão (sid), poderemos fazer uma pesquisa. O documento da sessão contém o ID do perfil. Se a pesquisa de sessão for bem-sucedida, salvaremos o ID do perfil (pid) na solicitação.
Em seguida, atualizamos a expiração da sessão e passamos pelo middleware e voltamos ao endpoint. Se a sessão não existir, nenhum ID de perfil será passado e a solicitação falhará.
Agora podemos usar nosso middleware para obter informações sobre nosso perfil em nosso endpoint de conta:
1 2 3 4 5 6 7 8 9 |
aplicativo.obter("/account", validar, assíncrono (solicitação, resposta) => { tentar { aguardar coleção.obter(solicitação.pid) .então((resultado) => resposta.enviar(resultado.valor)) .captura((e) => resposta.status(500).enviar(e)) } captura (e) { console.erro(e.mensagem) } }) |
Observe o validar
acontece primeiro e depois o restante da solicitação. A request.pid
foi estabelecido pelo middleware e ele nos fornecerá um documento de perfil específico para esse ID.
Em seguida, criamos um endpoint para adicionar um artigo de blog para o usuário:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
aplicativo.postagem("/blog", validar, assíncrono(solicitação, resposta) => { se(!solicitação.corpo.título) { retorno resposta.status(401).enviar({ "mensagem": "É necessário um `título`" }) } mais se(!solicitação.corpo.conteúdo) { retorno resposta.status(401).enviar({ "mensagem": "É necessário um `conteúdo`" }) } var blog = { "tipo": "blog", "pid": solicitação.pid, "título": solicitação.corpo.título, "content" (conteúdo): solicitação.corpo.conteúdo, "timestamp" (registro de data e hora): (novo Data()).getTime() } const uniqueId = uuid.v4() coleção.inserir(uniqueId, blog) .então(() => resposta.enviar(blog)) .captura((e) => resposta.status(500).enviar(e)) }) |
Supondo que o middleware tenha sido bem-sucedido, criamos um objeto de blog com um tipo
e pid
. Em seguida, podemos salvá-lo no banco de dados.
A consulta de todos os posts de blog de um determinado usuário não é muito diferente:
1 2 3 4 5 6 7 8 9 10 11 |
aplicativo.obter("/blogs", validar, assíncrono(solicitação, resposta) => { tentar { const consulta = `SELECT * DE `blog` ONDE tipo = 'blog' E pid = $PID;` const opções = { parâmetros: { PID: solicitação.pid } } aguardar agrupamento.consulta(consulta, opções) .então((resultado) => resposta.enviar(resultado.linhas)) .captura((e) => resposta.status(500).enviar(e)) } captura (e) { console.erro(e.mensagem) } }) |
Como precisamos consultar por propriedade do documento em vez de chave do documento, usaremos uma consulta e um índice N1QL que criamos anteriormente.
O documento tipo
e pid
são passados para a consulta que retorna todos os documentos para esse perfil específico.
Conclusão
Você acabou de ver como criar um armazenamento e uma sessão de perfil de usuário usando Node.js e NoSQL. Essa é uma ótima continuação da explicação de alto nível de Kirk em seu artigo artigo.
Conforme mencionado anteriormente, o conta poderiam representar uma forma de credenciais de login em que você poderia ter um documento para autenticação básica (autenticação do Facebook, etc.) referindo-se ao mesmo documento de perfil. Em vez de usar um UUID para a sessão, poderia ser usado um JSON Web Token (JWT) ou talvez algo mais seguro.
O O próximo tutorial desta série nos ajudará a criar um front-end de cliente para essa API.
O código finalizado, as coleções do Postman e as variáveis de ambiente disponíveis no couchbaselabs / couchbase-nodejs-blog-api no GitHub.
Para obter mais informações sobre como usar o Couchbase com o Node.js, consulte o Portal do desenvolvedor do Couchbase.
[...] Link: https://www.couchbase.com/creating-user-profile-store-with-node-js-nosql-database/ […]