auth code organization
This commit is contained in:
parent
e2e6ec541a
commit
855274afcc
6 changed files with 151 additions and 145 deletions
|
|
@ -5,7 +5,7 @@ import * as schema from './schema';
|
||||||
const db = drizzle({
|
const db = drizzle({
|
||||||
connection: {
|
connection: {
|
||||||
connectionString: process.env.DATABASE_URL!,
|
connectionString: process.env.DATABASE_URL!,
|
||||||
// ssl: true, // TODO!
|
ssl: process.env.NODE_ENV == 'production',
|
||||||
},
|
},
|
||||||
schema,
|
schema,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -1,152 +1,20 @@
|
||||||
import express, { Request, Response } from 'express';
|
import express from 'express';
|
||||||
import cookieParser from 'cookie-parser';
|
import cookieParser from 'cookie-parser';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import jwt from 'jsonwebtoken';
|
|
||||||
|
|
||||||
import db from './db';
|
import authRouter from './services/auth';
|
||||||
import { eq } from 'drizzle-orm';
|
|
||||||
import { usersTable } from './db/schema';
|
|
||||||
|
|
||||||
import { User } from './types';
|
|
||||||
|
|
||||||
import { argon2id, hash, verify } from 'argon2';
|
|
||||||
|
|
||||||
const HOST = process.env.HOST ?? 'localhost';
|
const HOST = process.env.HOST ?? 'localhost';
|
||||||
const PORT = process.env.PORT ? Number(process.env.PORT) : 3000;
|
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();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
app.use(cookieParser());
|
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());
|
app.use(express.json());
|
||||||
|
|
||||||
const refreshTokens = new Set<string>(); // TODO: ideally store in DB
|
app.use(`/auth`, authRouter);
|
||||||
|
|
||||||
// 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.listen(PORT, HOST, () => {
|
app.listen(PORT, HOST, () => {
|
||||||
console.log(`[ ready ] http://${HOST}:${PORT}`);
|
console.log(`[ ready ] http://${HOST}:${PORT}`);
|
||||||
|
|
|
||||||
141
apps/backend/src/services/auth.ts
Normal file
141
apps/backend/src/services/auth.ts
Normal 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;
|
||||||
|
|
@ -1,18 +1,15 @@
|
||||||
import { useAuthStore } from './authStore';
|
import { useAuthStore } from './authStore';
|
||||||
|
|
||||||
// TODO: remover?
|
|
||||||
export function getAccessToken(): string | null {
|
export function getAccessToken(): string | null {
|
||||||
return useAuthStore.getState().accessToken;
|
return useAuthStore.getState().accessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: precisa exportar?
|
function setAccessToken(token: string | null) {
|
||||||
export function setAccessToken(token: string | null) {
|
|
||||||
useAuthStore.getState().setAccessToken(token);
|
useAuthStore.getState().setAccessToken(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function refreshToken(): Promise<string> {
|
export async function refreshToken(): Promise<string> {
|
||||||
// TODO: verificar se há cookie para tentar o refresh?
|
const res = await fetch(`${import.meta.env.VITE_SERVER}/auth/refresh`, {
|
||||||
const res = await fetch(`${import.meta.env.VITE_SERVER}/refresh`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
@ -25,7 +22,7 @@ export async function refreshToken(): Promise<string> {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function logout() {
|
export async function logout() {
|
||||||
await fetch(`${import.meta.env.VITE_SERVER}/logout`, {
|
await fetch(`${import.meta.env.VITE_SERVER}/auth/logout`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
credentials: 'include',
|
credentials: 'include',
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default function HomePage() {
|
||||||
function Protected() {
|
function Protected() {
|
||||||
const callAPI = async () => {
|
const callAPI = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await api.get('/protected');
|
const res = await api.get('/auth/protected');
|
||||||
alert(res.data.message);
|
alert(res.data.message);
|
||||||
} catch {
|
} catch {
|
||||||
alert('Unauthorized');
|
alert('Unauthorized');
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export default function LoginPage() {
|
||||||
const [password, _password] = useState('');
|
const [password, _password] = useState('');
|
||||||
|
|
||||||
const login = async () => {
|
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',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|
|
||||||
Reference in a new issue