diff --git a/infra/controller.js b/infra/controller.js index da0019b..1a9117d 100644 --- a/infra/controller.js +++ b/infra/controller.js @@ -42,12 +42,24 @@ async function setSessionCookie(sessionToken, response) { response.setHeader("Set-Cookie", setCookie); } +async function clearSessionCookie(response) { + const setCookie = cookie.serialize("session_id", "invalid", { + path: "/", + maxAge: -1, + secure: process.env.NODE_ENV === "production", + httpOnly: true, + }); + + response.setHeader("Set-Cookie", setCookie); +} + const controller = { errorHandlers: { onNoMatch: onNoMatchHandler, onError: onErrorHandler, }, setSessionCookie, + clearSessionCookie, }; export default controller; diff --git a/models/session.js b/models/session.js index db23a08..d74bd0c 100644 --- a/models/session.js +++ b/models/session.js @@ -86,10 +86,35 @@ async function renew(sessionId) { } } +async function expireById(sessionId) { + const expiredSessionObject = await runUpdateQuery(sessionId); + return expiredSessionObject; + + async function runUpdateQuery(sessionId) { + const results = await database.query({ + text: ` + UPDATE + sessions + SET + expires_at = expires_at - interval '1 year', + updated_at = NOW() + WHERE + id = $1 + RETURNING + * + ;`, + values: [sessionId], + }); + + return results.rows[0]; + } +} + const session = { create, renew, findOneValidByToken, + expireById, EXPIRATION_IN_MILLISECONDS, }; diff --git a/pages/api/v1/sessions/index.js b/pages/api/v1/sessions/index.js index 435994d..331b4c9 100644 --- a/pages/api/v1/sessions/index.js +++ b/pages/api/v1/sessions/index.js @@ -1,5 +1,4 @@ import { createRouter } from "next-connect"; - import controller from "infra/controller"; import authentication from "models/authentication"; import session from "models/session"; @@ -7,6 +6,7 @@ import session from "models/session"; const router = createRouter(); router.post(postHandler); +router.delete(deleteHandler); export default router.handler(controller.errorHandlers); @@ -24,3 +24,13 @@ async function postHandler(request, response) { return response.status(201).json(newSession); } + +async function deleteHandler(resquest, response) { + const sessionToken = resquest.cookies.session_id; + + const sessionObject = await session.findOneValidByToken(sessionToken); + const expiredSession = await session.expireById(sessionObject.id); + controller.clearSessionCookie(response); + + return response.status(200).json(expiredSession); +} diff --git a/tests/integration/api/v1/sessions/delete.test.js b/tests/integration/api/v1/sessions/delete.test.js new file mode 100644 index 0000000..c6b6214 --- /dev/null +++ b/tests/integration/api/v1/sessions/delete.test.js @@ -0,0 +1,133 @@ +import { version as uuidVersion } from "uuid"; +import setCookieParser from "set-cookie-parser"; +import orchestrator from "../orchestrator"; +import session from "models/session"; + +beforeAll(async () => { + await orchestrator.waitForAllServices(); + await orchestrator.clearDatabase(); + await orchestrator.runPendingMigrations(); +}); + +describe("DELETE to /api/v1/sessions", () => { + describe("Default user", () => { + test("With nonexistent session", async () => { + const nonexistentToken = + "2d4f8f5c80a218d08cd8e4352a70e454e84204be7890ea68e5743bf2a76efd3ba2d1549694bc31d4a1767b8953ca0530"; + + const response = await fetch("http://localhost:3000/api/v1/sessions", { + method: "DELETE", + headers: { + Cookie: `session_id=${nonexistentToken}`, + }, + }); + + expect(response.status).toBe(401); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + name: "UnauthorizedError", + message: "Usuario não possui sessão ativa.", + action: "Verifique se este usuário está logado e tente novamente.", + statusCode: 401, + }); + }); + + test("With expired session", async () => { + jest.useFakeTimers({ + now: new Date(Date.now() - session.EXPIRATION_IN_MILLISECONDS), + }); + + const createUser = await orchestrator.createUser({ + username: "UserWithExpiredSession", + }); + + const sessionObject = await orchestrator.createSession(createUser.id); + + jest.useRealTimers(); + + const response = await fetch("http://localhost:3000/api/v1/sessions", { + method: "DELETE", + headers: { + Cookie: `session_id=${sessionObject.token}`, + }, + }); + + expect(response.status).toBe(401); + + const responseBody = await response.json(); + + expect(responseBody).toEqual({ + name: "UnauthorizedError", + message: "Usuario não possui sessão ativa.", + action: "Verifique se este usuário está logado e tente novamente.", + statusCode: 401, + }); + }); + + test("With valid session", async () => { + const createUser = await orchestrator.createUser({ + username: "UserWithValidSession", + }); + + const sessionObject = await orchestrator.createSession(createUser.id); + + const response = await fetch("http://localhost:3000/api/v1/sessions", { + method: "DELETE", + headers: { + Cookie: `session_id=${sessionObject.token}`, + }, + }); + expect(response.status).toBe(200); + + const responseBody = await response.json(); + expect(responseBody).toEqual({ + id: sessionObject.id, + token: sessionObject.token, + user_id: sessionObject.user_id, + expires_at: responseBody.expires_at, + created_at: responseBody.created_at, + updated_at: responseBody.updated_at, + }); + + expect(uuidVersion(responseBody.id)).toEqual(4); + expect(Date.parse(responseBody.expires_at)).not.toBeNaN(); + expect(Date.parse(responseBody.created_at)).not.toBeNaN(); + expect(Date.parse(responseBody.updated_at)).not.toBeNaN(); + + // Set-Cookie assertions + const parsedSetCookie = setCookieParser(response, { + map: true, + }); + + expect(parsedSetCookie.session_id).toEqual({ + name: "session_id", + value: "invalid", + maxAge: -1, + path: "/", + httpOnly: true, + }); + + // Double check assertions + const doubleCheckResponse = await fetch( + "http://localhost:3000/api/v1/user", + { + headers: { + Cookie: `session_id=${sessionObject.token}`, + }, + }, + ); + + expect(doubleCheckResponse.status).toBe(401); + + const doubleCheckResponseBody = await doubleCheckResponse.json(); + + expect(doubleCheckResponseBody).toEqual({ + name: "UnauthorizedError", + message: "Usuario não possui sessão ativa.", + action: "Verifique se este usuário está logado e tente novamente.", + statusCode: 401, + }); + }); + }); +});