-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from zleypner/Add-User-Authentication
feat:user-auth-api
- Loading branch information
Showing
11 changed files
with
276 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,4 +30,4 @@ jobs: | |
run: echo "Environment set to test" | ||
|
||
- name: Run Tests | ||
run: npm test | ||
run: npm test |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,4 +30,4 @@ const AppDataSource = new DataSource( | |
} | ||
); | ||
|
||
export default AppDataSource; | ||
export default AppDataSource; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -56,4 +56,4 @@ afterAll(async () => { | |
throw error; | ||
} | ||
} | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'; | ||
} | ||
} |