Skip to content

Commit

Permalink
Merge pull request #18 from zleypner/Add-User-Authentication
Browse files Browse the repository at this point in the history
feat:user-auth-api
  • Loading branch information
Villarley authored Dec 23, 2024
2 parents 2a36345 + 43d0415 commit c229a27
Show file tree
Hide file tree
Showing 11 changed files with 276 additions and 7 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ jobs:
run: echo "Environment set to test"

- name: Run Tests
run: npm test
run: npm test
11 changes: 11 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@nestjs/testing": "^10.4.13",
"@types/express": "^5.0.0",
"@types/jest": "^29.5.14",
"@types/jsonwebtoken": "^9.0.7",
"@types/node": "^22.10.1",
"@types/supertest": "^6.0.2",
"jest": "^29.7.0",
Expand Down
2 changes: 1 addition & 1 deletion src/config/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ const AppDataSource = new DataSource(
}
);

export default AppDataSource;
export default AppDataSource;
37 changes: 37 additions & 0 deletions src/controllers/auth.controlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// auth.controller.ts - Authentication Controller

// Import necessary types and services
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/auth.service';

/**
* Authentication Controller
* Handles login requests and user authentication
*/
export class AuthController {
/**
* Login Handler
* Processes user login attempts using wallet addresses
* Returns a JWT token if authentication is successful
*/
static async login(req: Request, res: Response, next: NextFunction) {
try {
// Extract wallet address from request body
const { walletAddress } = req.body;

// Check if wallet address was provided
if (!walletAddress) {
throw new Error('Wallet address is required');
}

// Authenticate user and get JWT token
const token = await AuthService.authenticateUser(walletAddress);

// Send token back to client
res.json({ token });
} catch (error) {
// Pass any errors to error handling middleware
next(error);
}
}
}
10 changes: 6 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import 'reflect-metadata';
import express from 'express';
import AppDataSource from './config/ormconfig';
import userRoutes from './routes/UserRoutes';

import router from './route/protected.routes';

const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());
app.use('/users', userRoutes);
// app.use('/users', userRoutes);

AppDataSource.initialize()
.then(() => {
Expand All @@ -26,4 +25,7 @@ app.get('/', (req, res) => {
});


export default app; // Export the app for testing
app.use('/', router)

console.log('JWT_SECRET:', process.env.JWT_SECRET); // Debug log (remove in production)

75 changes: 75 additions & 0 deletions src/middleware/auth.middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
// auth.middleware.ts - Authentication Middleware

// Import necessary types and services
import { Request, Response, NextFunction } from 'express';
import { AuthService } from '../services/auth.service';

/**
* Interface for extending the Express Request type
* Adds user information to the request object after authentication
*/
export interface AuthenticatedRequest extends Request {
user?: {
userId: number; // User's unique identifier
walletAddress: string; // User's blockchain wallet address
role: string; // User's role (e.g., 'user', 'admin')
};
}

/**
* Authentication Middleware
* Checks if the request has a valid JWT token
* If valid, adds user information to the request object
*/
export const authMiddleware = async (
req: AuthenticatedRequest,
res: Response,
next: NextFunction
) => {
try {
// Get the authorization header
const authHeader = req.headers.authorization;

// Check if authorization header exists and has correct format
if (!authHeader || !authHeader.startsWith('Bearer ')) {
throw new ReferenceError('No token provided');
}

// Extract the token from the header
// Format: "Bearer <token>"
const token = authHeader.split(' ')[1];

// Verify the token and decode its contents
const decoded = AuthService.verifyToken(token);


// Add user information to the request object
req.user = {
userId: decoded.userId,
walletAddress: decoded.walletAddress,
role: decoded.role
};

// Continue to the next middleware or route handler
next();
} catch (error) {
// Pass any errors to error handling middleware
next(error);
}
};

/**
* Role-based Access Control Middleware
* Checks if the authenticated user has the required role
* Must be used after authMiddleware
*/
export const requireRole = (role: string) => {
return (req: AuthenticatedRequest, res: Response, next: NextFunction) => {
// Check if user exists and has the required role
if (!req.user || req.user.role !== role) {
throw new ReferenceError('Insufficient permissions');
}
// If role matches, continue to next middleware or route handler
next();
};
};
31 changes: 31 additions & 0 deletions src/route/protected.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// protected.routes.ts - Protected Routes Configuration

// Import necessary packages and middleware
import { Router } from 'express'; // Express Router for handling routes
import { authMiddleware, requireRole } from '../middleware/auth.middleware'; // Authentication middleware

// Create a new router instance
const router = Router();

/**
* Protected Route - Basic User Access
* Route: GET /
* This route is protected and requires a valid authentication token
* Any authenticated user can access this route
*/
router.get('/test-token', authMiddleware, (req, res) => {
res.json({ message: 'Protected route accessed' });
});


/**
* Protected Route - Admin Only Access
* Route: GET /admin
* This route requires both authentication and admin role
* Only users with admin role can access this route
*/
router.get('/admin', authMiddleware, requireRole('admin'), (req, res) => {
res.json({ message: 'Admin route accessed' });
});

export default router;
61 changes: 61 additions & 0 deletions src/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// auth.service.ts - Authentication Service

// Import necessary packages and modules
import jwt from 'jsonwebtoken'; // Used for creating and verifying JWT tokens
import AppDataSource from '../config/ormconfig'; // Database connection
import { User } from '../entities/User'; // User model/entity

export class AuthService {
// Define constants for JWT configuration
// JWT_SECRET is used to sign and verify tokens - loaded from environment variables
private static readonly JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
// JWT_EXPIRES_IN sets how long the token is valid
private static readonly JWT_EXPIRES_IN = '24h';

/**
* Authenticates a user based on their wallet address
* If the user doesn't exist, creates a new user
* Returns a JWT token containing user information
*/
static async authenticateUser(walletAddress: string): Promise<string> {
// Get access to the User table in the database
const userRepository = AppDataSource.getRepository(User);

// Try to find an existing user with this wallet address
let user = await userRepository.findOne({ where: { walletAddress } });

// If no user exists, create a new one
if (!user) {
user = userRepository.create({ walletAddress });
await userRepository.save(user);
}

// Create a JWT token containing user information
const token = jwt.sign(
{
userId: user.id, // Include user ID in token
walletAddress: user.walletAddress, // Include wallet address
role: user.role // Include user role
},
this.JWT_SECRET, // Sign with secret key
{ expiresIn: this.JWT_EXPIRES_IN } // Set token expiration
);

return token;
}

/**
* Verifies if a JWT token is valid
* Returns the decoded token information if valid
* Throws an error if the token is invalid
*/
static verifyToken(token: string): any {
try {
// Attempt to verify the token
return jwt.verify(token, this.JWT_SECRET);
} catch (error) {
// If verification fails, throw an error
throw new ReferenceError('Invalid token');
}
}
}
2 changes: 1 addition & 1 deletion src/tests/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,4 @@ afterAll(async () => {
throw error;
}
}
});
});
51 changes: 51 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// errors.ts - Custom Error Classes

// UnauthorizedError
// Used when someone tries to access something they're not allowed to
// Like trying to enter a VIP area without a VIP pass
export class UnauthorizedError extends Error {
constructor(message: string = 'Unauthorized access') {
super(message); // Sends the message to the parent Error class
this.name = 'UnauthorizedError'; // Gives our error a specific name
}
}

// ValidationError
// Used when the data provided isn't in the correct format
// Like when someone types letters in a phone number field
export class ValidationError extends Error {
constructor(message: string = 'Validation failed') {
super(message);
this.name = 'ValidationError';
}
}

// NotFoundError
// Used when we try to find something that doesn't exist
// Like trying to find a page that was deleted
export class NotFoundError extends Error {
constructor(message: string = 'Resource not found') {
super(message);
this.name = 'NotFoundError';
}
}

// BadRequestError
// Used when a request is incorrectly formatted
// Like sending an empty form when all fields are required
export class BadRequestError extends Error {
constructor(message: string = 'Bad request') {
super(message);
this.name = 'BadRequestError';
}
}

// ReferenceValidationError
// Used when a reference to another piece of data is invalid
// Like trying to like a post that doesn't exist anymore
export class ReferenceValidationError extends Error {
constructor(message: string = 'Invalid reference') {
super(message);
this.name = 'ReferenceValidationError';
}
}

0 comments on commit c229a27

Please sign in to comment.