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
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
.next
.next
.vscode
70 changes: 62 additions & 8 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,69 @@
{
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[javascriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
"[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },

"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll.eslint": "never"
}
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll.eslint": "never"
}
},
"[javascriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll.eslint": "never"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll.eslint": "never"
}
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"editor.formatOnSave": true,
"files.autoSave": "off",
"editor.minimap.enabled": false,

"prettier.workingDirectory": "auto",
"prettier.useEditorConfig": true,
"prettier.requireConfig": false
"prettier.requireConfig": false,
// ⛔️ impede que o editor apague imports ao salvar
"editor.codeActionsOnSave": {
"source.organizeImports": "never",
"source.fixAll.eslint": "explicit"
},
// (opcional, redundante mas garantido por linguagem)
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false,
"source.fixAll.eslint": false
}
},
"[typescriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false,
"source.fixAll.eslint": false
}
},
"[javascript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false,
"source.fixAll.eslint": false
}
},
"[javascriptreact]": {
"editor.codeActionsOnSave": {
"source.organizeImports": false,
"source.fixAll.eslint": false
}
}
}
10 changes: 7 additions & 3 deletions infra/controller.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
InternalServerError,
MethodNotAllowedError,
NotFoundError,
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
26 changes: 24 additions & 2 deletions infra/errors.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export class ServiceError extends Error {
};
}
}

export class ValidationError extends Error {
constructor({ cause, message, action }) {
super(message || "Um erro de validação ocorreu.", {
Expand All @@ -56,13 +57,15 @@ export class ValidationError extends Error {
};
}
}

export class NotFoundError extends Error {
constructor({ cause, message, action }) {
super(message || "Não foi possível encontrar o recurso solicitado.", {
super(message || "Não foi possível encontrar este recurso no sistema.", {
cause,
});
this.name = "NotFoundError";
this.action = action || "Verifique se o recurso solicitado existe.";
this.action =
action || "Verifique se os parâmetros enviados na consulta estão certos.";
this.statusCode = 404;
}

Expand All @@ -75,6 +78,25 @@ export class NotFoundError extends Error {
};
}
}
export class UnauthorizedError extends Error {
constructor({ cause, message, action }) {
super(message || "Usuário 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,
status_code: this.statusCode,
};
}
}

export class MethodNotAllowedError extends Error {
constructor() {
Expand Down
41 changes: 41 additions & 0 deletions infra/migrations/1762199477615_create-sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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,
},

// Why timestamp with timezone? https://justatheory.com/2012/04/postgres-use-timestamptz/
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;
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 user from "models/user.js";
import password from "models/password.js";
import { NotFoundError, UnauthorizedError } from "infra/errors.js";

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: "Email não confere.",
action: "Verifique se este dado 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 este dado está 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;
Loading