|
@@ -0,0 +1,179 @@
|
|
|
|
|
+import { Router } from "express";
|
|
|
|
|
+import { z } from "zod";
|
|
|
|
|
+import bcrypt from "bcryptjs";
|
|
|
|
|
+import { db } from "../db/knex.js";
|
|
|
|
|
+import { config } from "../config/index.js";
|
|
|
|
|
+import { generateRefreshToken, hashRefreshToken, signAccessToken } from "../services/authTokens.js";
|
|
|
|
|
+
|
|
|
|
|
+export const authRouter = Router();
|
|
|
|
|
+
|
|
|
|
|
+const loginBodySchema = z.object({
|
|
|
|
|
+ login: z.string().min(1),
|
|
|
|
|
+ senha: z.string().min(1)
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const refreshBodySchema = z.object({
|
|
|
|
|
+ refreshToken: z.string().min(1)
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+const logoutBodySchema = z.object({
|
|
|
|
|
+ refreshToken: z.string().min(1).optional()
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+function toPublicUser(u) {
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: u.Id,
|
|
|
|
|
+ nome: u.Nome,
|
|
|
|
|
+ login: u.Login,
|
|
|
|
|
+ email: u.Email,
|
|
|
|
|
+ status: u.Status,
|
|
|
|
|
+ nivel: u.Nivel,
|
|
|
|
|
+ setor: u.Setor
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function isUserBlocked(u) {
|
|
|
|
|
+ const status = String(u?.Status ?? "").trim();
|
|
|
|
|
+ return status === "0";
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function issueTokensForUser(trx, user) {
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ const refreshToken = generateRefreshToken();
|
|
|
|
|
+ const refreshTokenHash = hashRefreshToken(refreshToken);
|
|
|
|
|
+ const expiresAt = new Date(now + config.jwt.refreshTtlSeconds * 1000);
|
|
|
|
|
+
|
|
|
|
|
+ await trx("refresh_tokens").insert({
|
|
|
|
|
+ UsuarioId: user.Id,
|
|
|
|
|
+ TokenHash: refreshTokenHash,
|
|
|
|
|
+ ExpiresAt: expiresAt
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const accessToken = signAccessToken({
|
|
|
|
|
+ sub: String(user.Id),
|
|
|
|
|
+ login: user.Login,
|
|
|
|
|
+ nivel: user.Nivel,
|
|
|
|
|
+ setor: user.Setor
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ return { accessToken, refreshToken };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+authRouter.post("/login", async (req, res, next) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const body = loginBodySchema.parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ const login = body.login.trim();
|
|
|
|
|
+ const isEmail = login.includes("@");
|
|
|
|
|
+
|
|
|
|
|
+ const user = await db("usuarios")
|
|
|
|
|
+ .where(isEmail ? { Email: login } : { Login: login })
|
|
|
|
|
+ .first();
|
|
|
|
|
+
|
|
|
|
|
+ if (!user) {
|
|
|
|
|
+ res.status(401).json({ error: "invalid_credentials" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isUserBlocked(user)) {
|
|
|
|
|
+ res.status(403).json({ error: "user_inactive" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const ok = await bcrypt.compare(body.senha, String(user.Senha ?? ""));
|
|
|
|
|
+ if (!ok) {
|
|
|
|
|
+ res.status(401).json({ error: "invalid_credentials" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tokens = await db.transaction(async (trx) => issueTokensForUser(trx, user));
|
|
|
|
|
+ res.json({ ...tokens, user: toPublicUser(user) });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ next(err);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+authRouter.post("/refresh", async (req, res, next) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const body = refreshBodySchema.parse(req.body);
|
|
|
|
|
+ const tokenHash = hashRefreshToken(body.refreshToken);
|
|
|
|
|
+
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+
|
|
|
|
|
+ const result = await db.transaction(async (trx) => {
|
|
|
|
|
+ const tokenRow = await trx("refresh_tokens").where({ TokenHash: tokenHash }).first();
|
|
|
|
|
+
|
|
|
|
|
+ if (!tokenRow) return null;
|
|
|
|
|
+ if (tokenRow.RevokedAt) return null;
|
|
|
|
|
+ if (new Date(tokenRow.ExpiresAt) <= now) return null;
|
|
|
|
|
+
|
|
|
|
|
+ const user = await trx("usuarios").where({ Id: tokenRow.UsuarioId }).first();
|
|
|
|
|
+ if (!user) return null;
|
|
|
|
|
+ if (isUserBlocked(user)) return null;
|
|
|
|
|
+
|
|
|
|
|
+ await trx("refresh_tokens").where({ Id: tokenRow.Id }).update({ RevokedAt: now });
|
|
|
|
|
+
|
|
|
|
|
+ const tokens = await issueTokensForUser(trx, user);
|
|
|
|
|
+ return { tokens, user };
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!result) {
|
|
|
|
|
+ res.status(401).json({ error: "invalid_refresh_token" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.json({ ...result.tokens, user: toPublicUser(result.user) });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ next(err);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+authRouter.post("/logout", async (req, res, next) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const body = logoutBodySchema.safeParse(req.body);
|
|
|
|
|
+ const refreshToken = body.success ? body.data.refreshToken : undefined;
|
|
|
|
|
+
|
|
|
|
|
+ const now = new Date();
|
|
|
|
|
+ const userId = req.user?.sub ? Number(req.user.sub) : null;
|
|
|
|
|
+
|
|
|
|
|
+ if (userId) {
|
|
|
|
|
+ await db("refresh_tokens")
|
|
|
|
|
+ .where({ UsuarioId: userId })
|
|
|
|
|
+ .whereNull("RevokedAt")
|
|
|
|
|
+ .update({ RevokedAt: now });
|
|
|
|
|
+ res.json({ ok: true });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (typeof refreshToken !== "string" || !refreshToken.trim()) {
|
|
|
|
|
+ res.status(400).json({ error: "refresh_token_required" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const tokenHash = hashRefreshToken(refreshToken.trim());
|
|
|
|
|
+ await db("refresh_tokens").where({ TokenHash: tokenHash }).update({ RevokedAt: now });
|
|
|
|
|
+ res.json({ ok: true });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ next(err);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+authRouter.get("/me", async (req, res, next) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const userId = req.user?.sub ? Number(req.user.sub) : null;
|
|
|
|
|
+ if (!userId) {
|
|
|
|
|
+ res.status(401).json({ error: "unauthorized" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const user = await db("usuarios").where({ Id: userId }).first();
|
|
|
|
|
+ if (!user) {
|
|
|
|
|
+ res.status(404).json({ error: "user_not_found" });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ res.json({ user: toPublicUser(user) });
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ next(err);
|
|
|
|
|
+ }
|
|
|
|
|
+});
|