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,
});
console.error(publicErrorObject);
Expand Down
20 changes: 20 additions & 0 deletions infra/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,23 @@ export class NotFoundError extends Error {
};
}
}

export class UnauthorizedError extends Error {
constructor({ cause, message, action }) {
super(message || "Usuário não autenticado.", {
cause: 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,
status_code: this.statusCode,
};
}
}
34 changes: 34 additions & 0 deletions infra/migrations/1751376079838_create-sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
58 changes: 58 additions & 0 deletions models/authentication.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import user from "./user";
import password from "./password";
import { NotFoundError, UnauthorizedError } from "infra/errors";

async function getAuthenticatedUser(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 autenticaçã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 este dado esta 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 este dado esta correto.",
});
}
}
}

const authentication = {
getAuthenticatedUser,
};

export default authentication;
35 changes: 35 additions & 0 deletions models/session.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.js";

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

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

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

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

return results.rows[0];
}
}

const session = {
create,
EXPIRATION_IN_MILLISECONDS,
};

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

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

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

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

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

Expand Down
17 changes: 17 additions & 0 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@faker-js/faker": "9.7.0",
"async-retry": "1.3.3",
"bcryptjs": "3.0.2",
"cookie": "1.0.2",
"dotenv": "16.5.0",
"dotenv-expand": "12.0.2",
"next": "15.3.1",
Expand All @@ -34,6 +35,7 @@
"pg": "8.15.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"set-cookie-parser": "2.7.1",
"swr": "2.3.3",
"uuid": "11.1.0"
},
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/session";

const router = createRouter();

router.post(postHandler);

export default router.handler(controller.errorHandlers);

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

const authenticatedUser = await authentication.getAuthenticatedUser(
userInputValues.email,
userInputValues.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