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({
|
||||
connection: {
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
// ssl: true, // TODO!
|
||||
ssl: process.env.NODE_ENV == 'production',
|
||||
},
|
||||
schema,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
|
|
|
|||
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';
|
||||
|
||||
// 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',
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
Reference in a new issue