auth code organization

This commit is contained in:
Ricardo Zylbergeld 2025-04-26 13:20:05 +03:00
parent e2e6ec541a
commit 855274afcc
6 changed files with 151 additions and 145 deletions

View file

@ -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,
});

View file

@ -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<string>(); // 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}`);

View file

@ -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<string>(); // 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;

View file

@ -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<string> {
// 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<string> {
}
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',
});

View file

@ -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');

View file

@ -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',