Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions infra/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import {
InternalServerError,
MethodNotAllowedError,
NotFoundError,
UnauthorizedErro,
UnauthorizedError,
ValidationError,
} from "infra/erros.js";

import * as cookie from "cookie";
import session from "models/session";

function onNoMatchHandler(request, response) {
const publicErrorObject = new MethodNotAllowedError();
response.status(publicErrorObject.statusCode).json(publicErrorObject);
Expand All @@ -15,7 +18,7 @@ function onErrorHandler(error, request, response) {
if (
error instanceof ValidationError ||
error instanceof NotFoundError ||
error instanceof UnauthorizedErro
error instanceof UnauthorizedError
) {
return response.status(error.statusCode).json(error);
}
Expand All @@ -29,11 +32,23 @@ function onErrorHandler(error, request, response) {
response.status(publicErrorObject.statusCode).json(publicErrorObject);
}

async function setSessionCookie(sessionToken, response) {
const setCookie = cookie.serialize("session_id", sessionToken, {
path: "/",
maxAge: session.EXPIRATION_IN_MILLISECONDS / 1000,
secure: process.env.NODE_ENV === "production",
httpOnly: true,
});

response.setHeader("Set-Cookie", setCookie);
}

const controller = {
errorHandlers: {
onNoMatch: onNoMatchHandler,
onError: onErrorHandler,
},
setSessionCookie,
};

export default controller;
4 changes: 2 additions & 2 deletions infra/erros.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,12 @@ export class NotFoundError extends Error {
}
}

export class UnauthorizedErro extends Error {
export class UnauthorizedError extends Error {
constructor({ cause, message, action }) {
super(message || "Usuário não autenticado.", {
cause,
});
this.name = "UnauthorizedErro";
this.name = "UnauthorizedError";
this.action = action || "Faça novamente o login para continuar.";
this.statusCode = 401;
}
Expand Down
10 changes: 5 additions & 5 deletions models/authentication.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import user from "models/user.js";
import password from "models/password.js";
import { NotFoundError, UnauthorizedErro } from "infra/erros";
import { NotFoundError, UnauthorizedError } from "infra/erros";

async function getAuthenticatedUser(providedEmail, providedPassword) {
try {
Expand All @@ -9,8 +9,8 @@ async function getAuthenticatedUser(providedEmail, providedPassword) {

return storedUser;
} catch (error) {
if (error instanceof UnauthorizedErro) {
throw new UnauthorizedErro({
if (error instanceof UnauthorizedError) {
throw new UnauthorizedError({
message: "Dados de autenticação não conferem.",
action: "Verifique se os dados enviados estão corretos.",
});
Expand All @@ -26,7 +26,7 @@ async function getAuthenticatedUser(providedEmail, providedPassword) {
storedUser = await user.findOneByEmail(providedEmail);
} catch (error) {
if (error instanceof NotFoundError) {
throw new UnauthorizedErro({
throw new UnauthorizedError({
message: "Email não confere.",
action: "Verifique se este dado está correto.",
});
Expand All @@ -45,7 +45,7 @@ async function getAuthenticatedUser(providedEmail, providedPassword) {
);

if (!correctPasswordMatch) {
throw new UnauthorizedErro({
throw new UnauthorizedError({
message: "Senha não confere.",
action: "Verifique se este dado está correto.",
});
Expand Down
60 changes: 60 additions & 0 deletions models/session.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,41 @@
import crypto from "node:crypto";
import database from "infra/database.js";
import { UnauthorizedError } from "infra/erros";

const EXPIRATION_IN_MILLISECONDS = 60 * 60 * 24 * 30 * 1000; // 30 Days

async function findOneValidByToken(sessionToken) {
const sessionFound = await runSelectQuery(sessionToken);

return sessionFound;

async function runSelectQuery(sessionToken) {
const results = await database.query({
text: `
SELECT
*
FROM
sessions
WHERE
token = $1
AND expires_at > NOW()
LIMIT
1
;`,
values: [sessionToken],
});

if (results.rowCount === 0) {
throw new UnauthorizedError({
message: "Usuário não possui sessão ativa.",
action: "Verifique se este usuário está logado e tente novamente.",
});
}

return results.rows[0];
}
}

async function create(userId) {
const token = crypto.randomBytes(48).toString("hex");
const expiresAt = new Date(Date.now() + EXPIRATION_IN_MILLISECONDS);
Expand All @@ -26,8 +59,35 @@ async function create(userId) {
}
}

async function renew(sessionId) {
const expiresAt = new Date(Date.now() + EXPIRATION_IN_MILLISECONDS);
const renewedSessionObject = runUpdateQuery(sessionId, expiresAt);
return renewedSessionObject;

async function runUpdateQuery(sessionId, expiresAt) {
const results = await database.query({
text: `
UPDATE
sessions
SET
expires_at = $2,
updated_at = NOW()
WHERE
id = $1
RETURNING
*
;`,
values: [sessionId, expiresAt],
});

return results.rows[0];
}
}

const session = {
create,
findOneValidByToken,
renew,
EXPIRATION_IN_MILLISECONDS,
};

Expand Down
32 changes: 32 additions & 0 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@ import database from "infra/database.js";
import password from "models/password.js";
import { NotFoundError, ValidationError } from "infra/erros.js";

async function findOneById(userId) {
const userFound = await runSelectQuery(userId);

return userFound;

async function runSelectQuery(userId) {
const results = await database.query({
text: `
SELECT
*
FROM
users
WHERE
id = $1
LIMIT
1
;`,
values: [userId],
});

if (results.rowCount === 0) {
throw new NotFoundError({
message: "O id informado não foi encontrado no sistema.",
action: "Verifique se o id está digitado corretamente.",
});
}

return results.rows[0];
}
}

async function findOneByUsername(username) {
const userFound = await runSelectQuery(username);

Expand Down Expand Up @@ -189,6 +220,7 @@ async function hashPasswordInObject(userInputValues) {

const user = {
create,
findOneById,
findOneByUsername,
findOneByEmail,
update,
Expand Down
9 changes: 1 addition & 8 deletions pages/api/v1/sessions/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import * as cookie from "cookie";
import { createRouter } from "next-connect";
import controller from "infra/controller.js";

Expand All @@ -21,13 +20,7 @@ async function postHandler(request, response) {

const newSession = await session.create(authenticatedUser.id);

const setCookie = cookie.serialize("session_id", newSession.token, {
path: "/",
maxAge: session.EXPIRATION_IN_MILLISECONDS / 1000,
secure: process.env.NODE_ENV === "production",
httpOnly: true,
});
response.setHeader("Set-Cookie", setCookie);
controller.setSessionCookie(newSession.token, response);

return response.status(201).json(newSession);
}
28 changes: 28 additions & 0 deletions pages/api/v1/user/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createRouter } from "next-connect";
import controller from "infra/controller.js";

import user from "models/user.js";
import session from "models/session";

const router = createRouter({});

router.get(getHandler);

export default router.handler(controller.errorHandlers);

async function getHandler(request, response) {
const sessionToken = request.cookies.session_id;

const sessionObject = await session.findOneValidByToken(sessionToken);
const renewedSessionObject = await session.renew(sessionObject.id);
controller.setSessionCookie(renewedSessionObject.token, response);

const userFound = await user.findOneById(sessionObject.user_id);

response.setHeader(
"Cache-Control",
"no-store, no-cache, max-age=0, must-revalidate",
);

return response.status(200).json(userFound);
}
6 changes: 3 additions & 3 deletions tests/integration/api/v1/sessions/post.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ describe("POST /api/v1/sessions", () => {
const responseBody = await response.json();

expect(responseBody).toEqual({
name: "UnauthorizedErro",
name: "UnauthorizedError",
message: "Dados de autenticação não conferem.",
action: "Verifique se os dados enviados estão corretos.",
status_code: 401,
Expand Down Expand Up @@ -60,7 +60,7 @@ describe("POST /api/v1/sessions", () => {
const responseBody = await response.json();

expect(responseBody).toEqual({
name: "UnauthorizedErro",
name: "UnauthorizedError",
message: "Dados de autenticação não conferem.",
action: "Verifique se os dados enviados estão corretos.",
status_code: 401,
Expand All @@ -86,7 +86,7 @@ describe("POST /api/v1/sessions", () => {
const responseBody = await response.json();

expect(responseBody).toEqual({
name: "UnauthorizedErro",
name: "UnauthorizedError",
message: "Dados de autenticação não conferem.",
action: "Verifique se os dados enviados estão corretos.",
status_code: 401,
Expand Down
Loading