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
8 changes: 6 additions & 2 deletions infra/controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
MethodNotAllowedError,
ValidationError,
NotFoundError,
UnauthorizedError,
} from "infra/errors";

function onNoMatchHandler(request, response) {
Expand All @@ -11,12 +12,15 @@ function onNoMatchHandler(request, response) {
}

function onErrorHandler(error, request, response) {
if (error instanceof ValidationError || error instanceof NotFoundError) {
if (
error instanceof ValidationError ||
error instanceof NotFoundError ||
error instanceof UnauthorizedError
) {
return response.status(error.statusCode).json(error);
}

const publicErrorObject = new InternalServerError({
statusCode: error.statusCode,
cause: error,
});

Expand Down
21 changes: 21 additions & 0 deletions infra/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,27 @@ export class NotFoundError extends Error {
}
}

export class UnauthorizedError extends Error {
constructor({ cause, message, action }) {
super(message || "Usuario não autenticado.", {
cause,
});

this.name = "UnauthorizedError";
this.action = action || "Faça novamente o login para continuar.";
this.statusCode = 401;
}

toJSON() {
return {
name: this.name,
message: this.message,
action: this.action,
statusCode: this.statusCode,
};
}
}

export class MethodNotAllowedError extends Error {
constructor() {
super("Metodo não permitido para este endpoint.");
Expand Down
39 changes: 39 additions & 0 deletions infra/migrations/1781379456775_create-sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
exports.up = (pgm) => {
pgm.createTable("sessions", {
id: {
type: "uuid",
primaryKey: true,
default: pgm.func("gen_random_uuid()"),
},

token: {
type: "varchar(96)",
notNull: true,
unique: true,
},

user_id: {
type: "uuid",
notNull: true,
},

expires_at: {
type: "timestamptz",
notNull: true,
},

created_at: {
type: "timestamptz",
notNull: true,
default: pgm.func("timezone('utc', now())"),
},

updated_at: {
type: "timestamptz",
notNull: true,
default: pgm.func("timezone('utc', now())"),
},
});
};

exports.down = false;
60 changes: 60 additions & 0 deletions models/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import password from "models/password";
import user from "models/user";
import { NotFoundError, UnauthorizedError } from "infra/errors";

async function getAuthenticateUser(providedEmail, providedPassword) {
try {
const storedUser = await findUserByEmail(providedEmail);
await validatePassword(providedPassword, storedUser.password);

return storedUser;
} catch (error) {
if (error instanceof UnauthorizedError) {
throw new UnauthorizedError({
message: "Dados de autonticação não conferem.",
action: "Verifique se os dados enviados estão corretos.",
});
}

throw error;
}

async function findUserByEmail(providedEmail) {
let storedUser;

try {
storedUser = await user.findOneByEmail(providedEmail);
} catch (error) {
if (error instanceof NotFoundError) {
throw new UnauthorizedError({
message: "Senha não confere.",
action: "Verifique se os dados está correto.",
});
}

throw error;
}

return storedUser;
}

async function validatePassword(providedPassword, storedPassword) {
const correctPasswordMatch = await password.compare(
providedPassword,
storedPassword,
);

if (!correctPasswordMatch) {
throw new UnauthorizedError({
message: "Senha não confere.",
action: "Verifique se os dados está correto.",
});
}
}
}

const authentication = {
getAuthenticateUser,
};

export default authentication;
35 changes: 35 additions & 0 deletions models/sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import crypto from "node:crypto";
import database from "infra/database";

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

async function create(userId) {
const token = crypto.randomBytes(48).toString("hex");
const expires_at = new Date(Date.now() + EXPIRATION_IN_MILLISECONDS);

const newSession = await runInsertQuery(token, userId, expires_at);
return newSession;

async function runInsertQuery(token, userId, expires_at) {
const results = await database.query({
text: `
INSERT INTO
sessions (token, user_id, expires_at)
VALUES
($1, $2, $3)
RETURNING
*
`,
values: [token, userId, expires_at],
});

return results.rows[0];
}
}

const session = {
create,
EXPIRATION_IN_MILLISECONDS,
};

export default session;
32 changes: 32 additions & 0 deletions models/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,37 @@ async function findOneByUsername(username) {
}
}

async function findOneByEmail(email) {
const userFound = await runSelectQuery(email);

return userFound;

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

if (result.rowCount === 0) {
throw new NotFoundError({
message: "O email informando não foi encotrado no sistema.",
action: "Verificque se o email está digitando corretamente.",
});
}

return result.rows[0];
}
}

async function create(userInputValue) {
await validateUniqueEmail(userInputValue.email);
await validateUniqueUsername(userInputValue.username);
Expand Down Expand Up @@ -158,6 +189,7 @@ async function hashPasswordInObject(userInputValue) {
const user = {
create,
findOneByUsername,
findOneByEmail,
update,
};

Expand Down
20 changes: 19 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"dependencies": {
"async-retry": "1.3.3",
"bcryptjs": "3.0.2",
"cookie": "1.0.2",
"dotenv": "17.4.2",
"dotenv-expand": "13.0.0",
"next": "16.2.6",
Expand Down Expand Up @@ -54,7 +55,8 @@
"husky": "9.1.7",
"jest": "30.4.2",
"npm-run-all": "^4.1.5",
"prettier": "3.8.3"
"prettier": "3.8.3",
"set-cookie-parser": "2.7.1"
},
"config": {
"commitizen": {
Expand Down
32 changes: 32 additions & 0 deletions pages/api/v1/sessions/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { createRouter } from "next-connect";
import * as cookie from "cookie";
import controller from "infra/controller";
import authentication from "models/authentication";
import session from "models/sessions";

const router = createRouter();

router.post(postHandler);

export default router.handler(controller.errorHandlers);

async function postHandler(request, response) {
const userInputValue = request.body;

const authenticatedUser = await authentication.getAuthenticateUser(
userInputValue.email,
userInputValue.password,
);

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);

return response.status(201).json(newSession);
}
Loading
Loading