diff --git a/apps/backend/src/db/index.ts b/apps/backend/src/db/index.ts index 07a9d14..e1de9bb 100644 --- a/apps/backend/src/db/index.ts +++ b/apps/backend/src/db/index.ts @@ -5,7 +5,7 @@ import * as schema from './schema'; const db = drizzle({ connection: { connectionString: process.env.DATABASE_URL!, - // ssl: true, // TODO! + ssl: process.env.NODE_ENV == 'production', }, schema, }); diff --git a/apps/backend/src/main.ts b/apps/backend/src/main.ts index 2497b75..886c310 100644 --- a/apps/backend/src/main.ts +++ b/apps/backend/src/main.ts @@ -1,152 +1,20 @@ -import express, { Request, Response } from 'express'; +import express from 'express'; import cookieParser from 'cookie-parser'; import cors from 'cors'; -import jwt from 'jsonwebtoken'; -import db from './db'; -import { eq } from 'drizzle-orm'; -import { usersTable } from './db/schema'; - -import { User } from './types'; - -import { argon2id, hash, verify } from 'argon2'; +import authRouter from './services/auth'; const HOST = process.env.HOST ?? 'localhost'; const PORT = process.env.PORT ? Number(process.env.PORT) : 3000; -const PASS_SECRET: string = process.env.SECRET || 'debug_secret'; -const ACCESS_SECRET = 'access-secret'; // TODO: env? -const REFRESH_SECRET = 'refresh-secret'; // TODO: env? const app = express(); app.use(express.json()); app.use(cookieParser()); -app.use(cors({ origin: 'http://localhost:4200', credentials: true })); // TODO: for dev purposes because it will be the same server on production? +app.use(cors({ origin: 'http://localhost:4200', credentials: true })); // for dev purposes because it will be the same server on production? app.use(express.json()); -const refreshTokens = new Set(); // TODO: ideally store in DB - -// TODO: simplify -const generateAccessToken = (user: User): string => { - return jwt.sign( - { - id: user.id, - name: user.name, - }, - ACCESS_SECRET, - { expiresIn: '15s' } - ); -}; -const generateRefreshToken = (user: User): string => { - return jwt.sign( - { - id: user.id, - name: user.name, - }, - REFRESH_SECRET, - { expiresIn: '7d' } - ); -}; - -// TODO: organize in services folder -app.post('/login', async (req: Request, res: Response) => { - const { email, password } = req.body; - - if (typeof email !== 'string' || typeof password !== 'string') { - return res.status(400).json({ error: 'Invalid request' }); - } - - const user = await db.query.usersTable.findFirst({ - where: eq(usersTable.email, email), - }); - - if ( - user == null || - !(await verify(user.hashed_password, password, { - // type: argon2id, - secret: Buffer.from(PASS_SECRET), - })) - ) { - res.sendStatus(400); - return; - } - - const refreshToken = generateRefreshToken(user); - refreshTokens.add(refreshToken); - - res.cookie('refreshToken', refreshToken, { - httpOnly: true, - secure: false, - sameSite: 'strict', - path: '/refresh', - }); - - const accessToken = generateAccessToken(user); - res.json({ accessToken }); -}); - -app.post('/refresh', (req: Request, res: Response) => { - const token = req.cookies?.refreshToken; - - // TODO: voltar à verificação abaixo, quando refresh tokens estiver em DB - // estudar melhor (exemplo, verificar expiração de refresh token etc) - if (!token /*|| !refreshTokens.has(token)*/) return res.sendStatus(403); - - try { - const user = jwt.verify(token, REFRESH_SECRET) as User; - const accessToken = generateAccessToken(user); - res.json({ accessToken }); - } catch (e) { - console.log(e); - res.sendStatus(403); - } -}); - -app.post('/logout', (req: Request, res: Response) => { - const token = req.cookies?.refreshToken; - if (token) refreshTokens.delete(token); - - res.clearCookie('refreshToken', { - httpOnly: true, - secure: false, - sameSite: 'strict', - path: '/refresh', - }); - - res.sendStatus(204); -}); - -app.get('/protected', (req: Request, res: Response) => { - // TODO: middleware? - const authHeader = req.headers['authorization']; - const token = authHeader?.split(' ')[1]; - - if (!token) return res.sendStatus(401); - - try { - const user = jwt.verify(token, ACCESS_SECRET) as User; - res.json({ message: `Hello ${user.name}` }); - } catch { - res.sendStatus(403); - } -}); - -// for development purposes - REMOVE -app.get('/hash', async (req: Request, res: Response) => { - const { password } = req.query; - - try { - const hashed_pass = await hash(password as string, { - type: argon2id, - secret: Buffer.from(PASS_SECRET), - }); - console.log(`hashed ${hashed_pass}`); - - res.sendStatus(200); - } catch (ex) { - res.sendStatus(500); - } -}); +app.use(`/auth`, authRouter); app.listen(PORT, HOST, () => { console.log(`[ ready ] http://${HOST}:${PORT}`); diff --git a/apps/backend/src/services/auth.ts b/apps/backend/src/services/auth.ts new file mode 100644 index 0000000..949db50 --- /dev/null +++ b/apps/backend/src/services/auth.ts @@ -0,0 +1,141 @@ +import { Router, Request, Response, NextFunction } from 'express'; + +import jwt from 'jsonwebtoken'; +import { eq } from 'drizzle-orm'; + +import type { StringValue } from "ms"; + +import db from '../db'; +import { usersTable } from '../db/schema'; + +import { User } from '../types'; + +import { argon2id, hash, verify } from 'argon2'; + +const PASS_SECRET: string = process.env.SECRET || 'debug_secret'; +const ACCESS_SECRET = process.env.ACCESS_SECRET || 'access-secret'; +const REFRESH_SECRET = process.env.REFRESH_SECRET || 'refresh-secret'; + +const refreshTokens = new Set(); // ideally store in DB + +const generateToken = (user: User, expiresIn: StringValue = '15s', secret: string = ACCESS_SECRET): string => { + return jwt.sign( + { + id: user.id, + name: user.name, + }, + secret, + { expiresIn } + ); +}; + +const router = Router(); + +// TODO: organize in services folder +router.post('/login', async (req: Request, res: Response) => { + const { email, password } = req.body; + + if (typeof email !== 'string' || typeof password !== 'string') { + return res.status(400).json({ error: 'Invalid request' }); + } + + const user = await db.query.usersTable.findFirst({ + where: eq(usersTable.email, email), + }); + + if ( + user == null || + !(await verify(user.hashed_password, password, { + // type: argon2id, + secret: Buffer.from(PASS_SECRET), + })) + ) { + res.sendStatus(400); + return; + } + + const refreshToken = generateToken(user, '7d', REFRESH_SECRET); + refreshTokens.add(refreshToken); + + res.cookie('refreshToken', refreshToken, { + httpOnly: true, + secure: false, + sameSite: 'strict', + path: '/refresh', + }); + + const accessToken = generateToken(user); + res.json({ accessToken }); +}); + +router.post('/refresh', (req: Request, res: Response) => { + const token = req.cookies?.refreshToken; + + // voltar à verificação abaixo, quando refresh tokens estiver em DB + if (!token /*|| !refreshTokens.has(token)*/) return res.sendStatus(403); + + try { + const user = jwt.verify(token, REFRESH_SECRET) as User; + const accessToken = generateToken(user); + res.json({ accessToken }); + } catch (e) { + console.log(e); + res.sendStatus(403); + } +}); + +router.post('/logout', (req: Request, res: Response) => { + const token = req.cookies?.refreshToken; + if (token) refreshTokens.delete(token); + + res.clearCookie('refreshToken', { + httpOnly: true, + secure: false, + sameSite: 'strict', + path: '/refresh', + }); + + res.sendStatus(204); +}); + +const verifyAuth = (req: Request, res: Response, next: NextFunction) => { + const authHeader = req.headers['authorization']; + const token = authHeader?.split(' ')[1]; + + if (!token) return res.sendStatus(401); + + try { + res.locals.user = jwt.verify(token, ACCESS_SECRET) as User; + next(); + } catch { + res.sendStatus(401); + } +}; + +router.get('/protected', verifyAuth, (req: Request, res: Response) => { + try { + const user = res.locals.user; + res.json({ message: `Hello ${user.name}` }); + } catch { + res.sendStatus(403); + } +}); + +// for development purposes - REMOVE +router.get('/hash', async (req: Request, res: Response) => { + const { password } = req.query; + + try { + const hashed_pass = await hash(password as string, { + type: argon2id, + secret: Buffer.from(PASS_SECRET), + }); + console.log(`hashed ${hashed_pass}`); + + res.sendStatus(200); + } catch (ex) { + res.sendStatus(500); + } +}); + +export default router; \ No newline at end of file diff --git a/apps/frontend/src/auth/auth-utils.ts b/apps/frontend/src/auth/auth-utils.ts index 5b8416d..066afc9 100644 --- a/apps/frontend/src/auth/auth-utils.ts +++ b/apps/frontend/src/auth/auth-utils.ts @@ -1,18 +1,15 @@ import { useAuthStore } from './authStore'; -// TODO: remover? export function getAccessToken(): string | null { return useAuthStore.getState().accessToken; } -// TODO: precisa exportar? -export function setAccessToken(token: string | null) { +function setAccessToken(token: string | null) { useAuthStore.getState().setAccessToken(token); } export async function refreshToken(): Promise { - // TODO: verificar se há cookie para tentar o refresh? - const res = await fetch(`${import.meta.env.VITE_SERVER}/refresh`, { + const res = await fetch(`${import.meta.env.VITE_SERVER}/auth/refresh`, { method: 'POST', credentials: 'include', }); @@ -25,7 +22,7 @@ export async function refreshToken(): Promise { } export async function logout() { - await fetch(`${import.meta.env.VITE_SERVER}/logout`, { + await fetch(`${import.meta.env.VITE_SERVER}/auth/logout`, { method: 'POST', credentials: 'include', }); diff --git a/apps/frontend/src/components/pages/HomePage.tsx b/apps/frontend/src/components/pages/HomePage.tsx index 9851294..b23374f 100644 --- a/apps/frontend/src/components/pages/HomePage.tsx +++ b/apps/frontend/src/components/pages/HomePage.tsx @@ -14,7 +14,7 @@ export default function HomePage() { function Protected() { const callAPI = async () => { try { - const res = await api.get('/protected'); + const res = await api.get('/auth/protected'); alert(res.data.message); } catch { alert('Unauthorized'); diff --git a/apps/frontend/src/components/pages/LoginPage.tsx b/apps/frontend/src/components/pages/LoginPage.tsx index 6ff458c..e878f2d 100644 --- a/apps/frontend/src/components/pages/LoginPage.tsx +++ b/apps/frontend/src/components/pages/LoginPage.tsx @@ -12,7 +12,7 @@ export default function LoginPage() { const [password, _password] = useState(''); const login = async () => { - const res = await fetch(`${import.meta.env.VITE_SERVER}/login`, { + const res = await fetch(`${import.meta.env.VITE_SERVER}/auth/login`, { method: 'POST', headers: { 'Content-Type': 'application/json',