auth: controllers + jwt riggings

Pedro Lucas Porcellis porcellis@eletrotupi.com 2 months ago 30e8babc0ef57b3aad96d854007a54810eb9e1fc
Parents: 90f9ece
7 file(s) changed
  • api/.env.example +2 -0
  • api/src/controllers/auth.ts +123 -0
  • api/src/controllers/users.ts +1 -1
  • api/src/index.ts +14 -0
  • api/src/lib/jwt.ts +33 -0
  • api/src/middleware/auth.ts +32 -0
  • api/src/routes/auth.ts +10 -0
api/.env.example
@@ -1,1 +1,3 @@
1 1 DATABASE_URL="file:./orbit.db"
2 + JWT_SECRET=your-secret-key-here-change-in-production
3 + FRONTEND_URL=http://192.168.3.96:8000
api/src/controllers/auth.ts
@@ -0,0 +1,124 @@
1 + import { Request, Response, NextFunction } from 'express';
2 + import {
3 + authenticateUser,
4 + generateToken,
5 + initiatePasswordReset,
6 + resetPassword
7 + } from '@app/models/user';
8 + import {
9 + validateRequiredFields
10 + } from '@app/utils/validators';
11 + import { verifyToken } from '@app/lib/jwt';
12 +
13 + export const AuthController = {
14 + login: async (req: Request, res: Response, next: NextFunction) => {
15 + try {
16 + const { email, password } = req.body;
17 +
18 + const requiredValidation = validateRequiredFields(req.body, [
19 + 'email', 'password'
20 + ]);
21 +
22 + if (!requiredValidation.valid) {
23 + return res.status(400).json({
24 + error: 'Missing required fields',
25 + missing: requiredValidation.missing
26 + });
27 + }
28 +
29 + const user = await authenticateUser(email, password);
30 + const jwtToken = generateToken(user.id, user.email);
31 +
32 + res.json({
33 + token: jwtToken,
34 + user: {
35 + id: user.id,
36 + email: user.email,
37 + firstName: user.firstName,
38 + lastName: user.lastName
39 + }
40 + });
41 + } catch (err: any) {
42 + if (err.message === 'Invalid credentials') {
43 + return res.status(401).json({ error: 'Invalid credentials' });
44 + }
45 + next(err);
46 + }
47 + },
48 +
49 + forgotPassword: async (req: Request, res: Response, next: NextFunction) => {
50 + try {
51 + const { email } = req.body;
52 +
53 + const requiredValidation = validateRequiredFields(req.body, ['email']);
54 +
55 + if (!requiredValidation.valid) {
56 + return res.status(400).json({
57 + error: 'Email is required',
58 + missing: requiredValidation.missing
59 + });
60 + }
61 +
62 + const result = await initiatePasswordReset(email);
63 +
64 + // In production, don't send token in response
65 + // Instead, send email with reset link
66 + res.json({
67 + message: 'Password reset instructions sent to your email',
68 + ...(!process.env.NODE_ENV || process.env.NODE_ENV === 'development' ? { token: result.token } : {})
69 + });
70 + } catch (err) {
71 + next(err);
72 + }
73 + },
74 +
75 + resetPassword: async (req: Request, res: Response, next: NextFunction) => {
76 + try {
77 + const { token, password } = req.body;
78 +
79 + const requiredValidation = validateRequiredFields(req.body, [
80 + 'token', 'password'
81 + ]);
82 +
83 + if (!requiredValidation.valid) {
84 + return res.status(400).json({
85 + error: 'Missing required fields',
86 + missing: requiredValidation.missing
87 + });
88 + }
89 +
90 + await resetPassword(token, password);
91 +
92 + res.json({ message: 'Password successfully reset' });
93 + } catch (err: any) {
94 + if (err.message.includes('token')) {
95 + return res.status(400).json({ error: err.message });
96 + }
97 + next(err);
98 + }
99 + },
100 +
101 + verify: async (req: Request, res: Response, next: NextFunction) => {
102 + try {
103 + const authHeader = req.headers.authorization;
104 +
105 + if (!authHeader || !authHeader.startsWith('Bearer ')) {
106 + return res.status(401).json({ error: 'No token provided' });
107 + }
108 +
109 + const token = authHeader.substring(7);
110 + const payload = verifyToken(token);
111 +
112 + res.json({
113 + valid: true,
114 + userId: payload.userId,
115 + email: payload.email
116 + });
117 + } catch (err: any) {
118 + if (err.name === 'JsonWebTokenError' || err.name === 'TokenExpiredError') {
119 + return res.status(401).json({ error: 'Invalid or expired token' });
120 + }
121 + next(err);
122 + }
123 + }
124 + };
api/src/controllers/users.ts
@@ -29,7 +29,7 @@ const user = await createUser(
29 29 email, firstName, lastName, password
30 30 );
31 31
32 - const jwtToken = generateToken(user.id);
32 + const jwtToken = generateToken(user.id, user.email);
33 33
34 34 res.status(201).json({ token: jwtToken });
35 35 } catch (err) {
api/src/index.ts
@@ -1,13 +1,27 @@
1 1 import express from 'express';
2 + import cors from 'cors';
3 + import morgan from 'morgan';
2 4 import userRouter from '@app/routes/users'
5 + import authRouter from '@app/routes/auth'
3 6 import { errorHandler } from '@app/middleware/errorHandler';
4 7
5 8 const port = process.env.PORT || 3000;
6 9
7 10 const app = express();
11 +
12 + // Configure CORS
13 + // Fucking expo
14 + app.use(cors({
15 + //origin: process.env.FRONTEND_URL || 'http://localhost:8081', // Expo default port
16 + origin: "*",
17 + credentials: true
18 + }));
19 +
8 20 app.use(express.json())
21 + app.use(morgan('combined'))
9 22
10 23 app.use("/users", userRouter);
24 + app.use("/auth", authRouter);
11 25
12 26 app.use(errorHandler); // always last
13 27
api/src/lib/jwt.ts
@@ -0,0 +1,34 @@
1 + import jwt from 'jsonwebtoken';
2 + import crypto from 'crypto';
3 +
4 + const JWT_SECRET = process.env.JWT_SECRET || 'development-secret-change-in-production';
5 + const JWT_EXPIRES_IN = '7d';
6 + const PASSWORD_RESET_EXPIRES_IN = '1h';
7 +
8 + interface TokenPayload {
9 + userId: number;
10 + email: string;
11 + }
12 +
13 + export const generateToken = (userId: number, email: string): string => {
14 + const payload: TokenPayload = { userId, email };
15 + return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
16 + };
17 +
18 + export const verifyToken = (token: string): TokenPayload => {
19 + return jwt.verify(token, JWT_SECRET) as TokenPayload;
20 + };
21 +
22 + export const generatePasswordResetToken = (): string => {
23 + return crypto.randomBytes(32).toString('hex');
24 + };
25 +
26 + export const getPasswordResetExpiration = (): Date => {
27 + const expirationTime = new Date();
28 + expirationTime.setHours(expirationTime.getHours() + 1); // 1 hour from now
29 + return expirationTime;
30 + };
31 +
32 + export const isTokenExpired = (expirationDate: Date): boolean => {
33 + return new Date() > expirationDate;
34 + };
api/src/middleware/auth.ts
@@ -0,0 +1,33 @@
1 + import { Request, Response, NextFunction } from 'express';
2 + import { verifyToken } from '@app/lib/jwt';
3 +
4 + export interface AuthenticatedRequest extends Request {
5 + userId?: number;
6 + userEmail?: string;
7 + }
8 +
9 + export const requireAuth = (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
10 + try {
11 + const authHeader = req.headers.authorization;
12 +
13 + if (!authHeader || !authHeader.startsWith('Bearer ')) {
14 + return res.status(401).json({ error: 'Unauthorized: No token provided' });
15 + }
16 +
17 + const token = authHeader.substring(7);
18 + const payload = verifyToken(token);
19 +
20 + req.userId = payload.userId;
21 + req.userEmail = payload.email;
22 +
23 + next();
24 + } catch (err: any) {
25 + if (err.name === 'JsonWebTokenError') {
26 + return res.status(401).json({ error: 'Unauthorized: Invalid token' });
27 + }
28 + if (err.name === 'TokenExpiredError') {
29 + return res.status(401).json({ error: 'Unauthorized: Token expired' });
30 + }
31 + next(err);
32 + }
33 + };
api/src/routes/auth.ts
@@ -0,0 +1,11 @@
1 + import { Router } from 'express';
2 + import { AuthController } from '@app/controllers/auth';
3 +
4 + const router = Router();
5 +
6 + router.post('/login', AuthController.login);
7 + router.post('/forgot-password', AuthController.forgotPassword);
8 + router.post('/reset-password', AuthController.resetPassword);
9 + router.get('/verify', AuthController.verify);
10 +
11 + export default router;