diff --git a/API_DOCUMENTATION.md b/API_DOCUMENTATION.md new file mode 100644 index 0000000..00be9cf --- /dev/null +++ b/API_DOCUMENTATION.md @@ -0,0 +1,229 @@ +# ๐Ÿ” RBAC API Documentation + +## Authentication Endpoints + +### POST /api/auth/register +Register a new user in the system. + +**Request Body:** +```json +{ + "username": "string", + "email": "string", + "fullname": "string", + "password": "string" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "User registered successfully", + "user": { + "id": "user_id", + "username": "username", + "email": "email", + "role": "User" + } +} +``` + +### POST /api/auth/login +Authenticate user and return access and refresh tokens. + +**Request Body:** +```json +{ + "email": "string", + "password": "string" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Login successful", + "accessToken": "jwt_access_token", + "refreshToken": "jwt_refresh_token", + "user": { + "id": "user_id", + "username": "username", + "email": "email", + "fullname": "fullname", + "role": "User" + } +} +``` + +### POST /api/auth/refresh +Refresh access token using refresh token. + +**Request Body:** +```json +{ + "refreshToken": "jwt_refresh_token" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Token refreshed successfully", + "accessToken": "new_jwt_access_token", + "user": { + "id": "user_id", + "username": "username", + "email": "email", + "fullname": "fullname", + "role": "User" + } +} +``` + +### POST /api/auth/logout +Logout user and invalidate refresh token. + +**Request Body:** +```json +{ + "refreshToken": "jwt_refresh_token" +} +``` + +**Response:** +```json +{ + "success": true, + "message": "Logged out successfully" +} +``` + +## Role Management Endpoints + +### GET /api/roles +Get all roles (requires authentication). + +**Headers:** +``` +Authorization: Bearer +``` + +**Response:** +```json +[ + { + "_id": "role_id", + "name": "Admin", + "permissions": [ + { + "_id": "permission_id", + "name": "Manage Users", + "description": "Admin can manage users" + } + ] + } +] +``` + +### POST /api/roles +Create a new role (requires authentication). + +**Headers:** +``` +Authorization: Bearer +``` + +**Request Body:** +```json +{ + "name": "string", + "permissions": ["permission_id_1", "permission_id_2"] +} +``` + +## Permission Management Endpoints + +### GET /api/permissions +Get all permissions (requires authentication). + +**Headers:** +``` +Authorization: Bearer +``` + +### POST /api/permissions +Create a new permission (requires authentication). + +**Headers:** +``` +Authorization: Bearer +``` + +**Request Body:** +```json +{ + "name": "string", + "description": "string" +} +``` + +## RBAC Test Endpoints + +### GET /api/rbac-test/admin-only +Test endpoint for Admin role only. + +**Headers:** +``` +Authorization: Bearer +``` + +**Response:** +```json +{ + "message": "Welcome, Admin" +} +``` + +### GET /api/rbac-test/user-only +Test endpoint for User role only. + +**Headers:** +``` +Authorization: Bearer +``` + +**Response:** +```json +{ + "message": "Welcome, User" +} +``` + +## Error Responses + +All endpoints return consistent error responses: + +```json +{ + "success": false, + "message": "Error description" +} +``` + +Common HTTP status codes: +- `400` - Bad Request +- `401` - Unauthorized +- `403` - Forbidden +- `404` - Not Found +- `500` - Internal Server Error + +## Security Features + +- **JWT Access Tokens**: Short-lived (1 day) for API access +- **Refresh Tokens**: Long-lived (7 days) for token renewal +- **Password Hashing**: bcrypt with salt rounds +- **Role-Based Access Control**: Granular permissions +- **Token Invalidation**: Secure logout mechanism diff --git a/README.md b/README.md index 7e0bf2b..bc35048 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ This project is developed and maintained under **Opcode, IIIT Bhagalpur** ๐Ÿš€. ## ๐ŸŒŸ Features - โœ… User authentication with **JWT** +- โœ… **Refresh Token mechanism** for persistent login - โœ… Secure password hashing (**bcrypt**) - โœ… Role-based access (Admin, User, Moderator, etc.) - โœ… Permission-based middleware for fine-grained access @@ -59,10 +60,25 @@ npm install ### 3๏ธโƒฃ Setup Environment -``` +Create a `.env` file in the root directory with the following variables: + +```env +# Server Configuration PORT=5000 + +# Database Configuration MONGO_URI=mongodb://localhost:27017/rbac -JWT_SECRET=your-secret-key + +# JWT Configuration +JWT_SECRET=your-super-secret-jwt-key-here +JWT_EXPIRY=1d + +# Refresh Token Configuration +REFRESH_TOKEN_SECRET=your-super-secret-refresh-token-key-here +REFRESH_TOKEN_EXPIRY=7d + +# CORS Configuration +CORS_URL=http://localhost:3000 ``` ### 4๏ธโƒฃ Run the Project @@ -71,6 +87,76 @@ JWT_SECRET=your-secret-key npm run dev ``` +### 5๏ธโƒฃ Seed the Database + +Before using the application, seed the database with default roles and permissions: + +```bash +node src/seed/seedRoles.js +``` + +--- + +## ๐Ÿ”Œ API Endpoints + +### Authentication Endpoints + +| Method | Endpoint | Description | Body | +|--------|----------|-------------|------| +| POST | `/api/auth/register` | Register a new user | `{username, email, fullname, password}` | +| POST | `/api/auth/login` | Login user | `{email, password}` | +| POST | `/api/auth/refresh` | Refresh access token | `{refreshToken}` | +| POST | `/api/auth/logout` | Logout user | `{refreshToken}` | + +### Role Management Endpoints + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| GET | `/api/roles` | Get all roles | Yes | +| POST | `/api/roles` | Create new role | Yes | +| GET | `/api/roles/:id` | Get role by ID | Yes | +| PUT | `/api/roles/:id` | Update role | Yes | +| DELETE | `/api/roles/:id` | Delete role | Yes | +| PUT | `/api/roles/:id/permissions` | Assign permissions to role | Yes | + +### Permission Management Endpoints + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| GET | `/api/permissions` | Get all permissions | Yes | +| POST | `/api/permissions` | Create new permission | Yes | +| GET | `/api/permissions/:id` | Get permission by ID | Yes | +| PUT | `/api/permissions/:id` | Update permission | Yes | +| DELETE | `/api/permissions/:id` | Delete permission | Yes | + +### RBAC Test Endpoints + +| Method | Endpoint | Description | Auth Required | +|--------|----------|-------------|---------------| +| GET | `/api/rbac-test/admin-only` | Admin only access | Yes (Admin role) | +| GET | `/api/rbac-test/user-only` | User only access | Yes (User role) | + +--- + +## ๐Ÿ”„ Authentication Flow + +### Login Flow +1. User sends credentials to `/api/auth/login` +2. Server validates credentials +3. Server generates both access token (short-lived) and refresh token (long-lived) +4. Both tokens are returned to client + +### Token Refresh Flow +1. When access token expires, client sends refresh token to `/api/auth/refresh` +2. Server validates refresh token +3. Server generates new access token +4. New access token is returned to client + +### Logout Flow +1. Client sends refresh token to `/api/auth/logout` +2. Server invalidates the refresh token in database +3. Client should discard both tokens + --- ### ๐Ÿ”„ System Flows diff --git a/TESTING_GUIDE.md b/TESTING_GUIDE.md new file mode 100644 index 0000000..dadebf0 --- /dev/null +++ b/TESTING_GUIDE.md @@ -0,0 +1,387 @@ +# ๐Ÿงช RBAC Project Testing Guide + +This guide will help you test the complete functionality of the RBAC project, including the new refresh token mechanism. + +## ๐Ÿ“‹ Prerequisites + +1. **Install Dependencies** + ```bash + npm install + ``` + +2. **Set up Environment Variables** + Create a `.env` file in the root directory: + ```env + PORT=5000 + MONGO_URI=mongodb://localhost:27017/rbac + JWT_SECRET=your-super-secret-jwt-key-here + JWT_EXPIRY=1d + REFRESH_TOKEN_SECRET=your-super-secret-refresh-token-key-here + REFRESH_TOKEN_EXPIRY=7d + CORS_URL=http://localhost:3000 + ``` + +3. **Start MongoDB** + Make sure MongoDB is running on your system. + +## ๐Ÿš€ Step-by-Step Testing + +### Step 1: Start the Server +```bash +npm run dev +``` +You should see: `Server is running at port : 5000` + +### Step 2: Seed the Database +```bash +node src/seed/seedRoles.js +``` +You should see: +``` +Connected to MongoDB +Created permission: User Actions +Created permission: Manage Users +Created permission: Manage Roles +Created role: Admin +Created role: User +Seeding completed! +``` + +### Step 3: Test the API Endpoints + +#### 3.1 Test Server Health +```bash +curl http://localhost:5000/ +``` +Expected response: `RBAC is running...` + +#### 3.2 Test User Registration +```bash +curl -X POST http://localhost:5000/api/auth/register \ + -H "Content-Type: application/json" \ + -d '{ + "username": "testuser", + "email": "test@example.com", + "fullname": "Test User", + "password": "password123" + }' +``` + +Expected response: +```json +{ + "success": true, + "message": "User registered successfully", + "user": { + "id": "user_id", + "username": "testuser", + "email": "test@example.com", + "role": "User" + } +} +``` + +#### 3.3 Test User Login (with Refresh Token) +```bash +curl -X POST http://localhost:5000/api/auth/login \ + -H "Content-Type: application/json" \ + -d '{ + "email": "test@example.com", + "password": "password123" + }' +``` + +Expected response: +```json +{ + "success": true, + "message": "Login successful", + "accessToken": "jwt_token_here", + "refreshToken": "refresh_token_here", + "user": { + "id": "user_id", + "username": "testuser", + "email": "test@example.com", + "fullname": "Test User", + "role": "User" + } +} +``` + +**Save the tokens for next steps!** + +#### 3.4 Test Protected Endpoint +```bash +curl -X GET http://localhost:5000/api/rbac-test/user-only \ + -H "Authorization: Bearer YOUR_ACCESS_TOKEN" +``` + +Expected response: +```json +{ + "message": "Welcome, User" +} +``` + +#### 3.5 Test Token Refresh +```bash +curl -X POST http://localhost:5000/api/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{ + "refreshToken": "YOUR_REFRESH_TOKEN" + }' +``` + +Expected response: +```json +{ + "success": true, + "message": "Token refreshed successfully", + "accessToken": "new_jwt_token_here", + "user": { + "id": "user_id", + "username": "testuser", + "email": "test@example.com", + "fullname": "Test User", + "role": "User" + } +} +``` + +#### 3.6 Test Logout +```bash +curl -X POST http://localhost:5000/api/auth/logout \ + -H "Content-Type: application/json" \ + -d '{ + "refreshToken": "YOUR_REFRESH_TOKEN" + }' +``` + +Expected response: +```json +{ + "success": true, + "message": "Logged out successfully" +} +``` + +#### 3.7 Test Refresh After Logout (Should Fail) +```bash +curl -X POST http://localhost:5000/api/auth/refresh \ + -H "Content-Type: application/json" \ + -d '{ + "refreshToken": "YOUR_REFRESH_TOKEN" + }' +``` + +Expected response: +```json +{ + "success": false, + "message": "Invalid refresh token" +} +``` + +## ๐Ÿ”ง Using Postman/Insomnia + +### Collection Setup +1. Create a new collection called "RBAC API" +2. Set base URL: `http://localhost:5000/api` + +### Request Examples + +#### 1. Register User +- **Method**: POST +- **URL**: `{{base_url}}/auth/register` +- **Body** (JSON): +```json +{ + "username": "testuser", + "email": "test@example.com", + "fullname": "Test User", + "password": "password123" +} +``` + +#### 2. Login User +- **Method**: POST +- **URL**: `{{base_url}}/auth/login` +- **Body** (JSON): +```json +{ + "email": "test@example.com", + "password": "password123" +} +``` + +#### 3. Test Protected Route +- **Method**: GET +- **URL**: `{{base_url}}/rbac-test/user-only` +- **Headers**: + - `Authorization`: `Bearer {{accessToken}}` + +#### 4. Refresh Token +- **Method**: POST +- **URL**: `{{base_url}}/auth/refresh` +- **Body** (JSON): +```json +{ + "refreshToken": "{{refreshToken}}" +} +``` + +#### 5. Logout +- **Method**: POST +- **URL**: `{{base_url}}/auth/logout` +- **Body** (JSON): +```json +{ + "refreshToken": "{{refreshToken}}" +} +``` + +## ๐Ÿงช Automated Testing Script + +Create a test script to verify all functionality: + +```javascript +// test-api.js +const BASE_URL = 'http://localhost:5000/api'; + +async function testAPI() { + console.log('๐Ÿงช Starting API Tests...\n'); + + // Test 1: Health Check + console.log('1๏ธโƒฃ Testing server health...'); + const health = await fetch('http://localhost:5000/'); + console.log('โœ… Server is running:', await health.text()); + + // Test 2: Register User + console.log('\n2๏ธโƒฃ Testing user registration...'); + const registerResponse = await fetch(`${BASE_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: 'testuser', + email: 'test@example.com', + fullname: 'Test User', + password: 'password123' + }) + }); + + if (registerResponse.ok) { + console.log('โœ… User registration successful'); + } else { + console.log('โš ๏ธ User might already exist'); + } + + // Test 3: Login + console.log('\n3๏ธโƒฃ Testing user login...'); + const loginResponse = await fetch(`${BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'test@example.com', + password: 'password123' + }) + }); + + const loginData = await loginResponse.json(); + if (loginData.success) { + console.log('โœ… Login successful'); + console.log('๐Ÿ“ Access Token:', loginData.accessToken.substring(0, 30) + '...'); + console.log('๐Ÿ”„ Refresh Token:', loginData.refreshToken.substring(0, 30) + '...'); + + // Test 4: Protected Route + console.log('\n4๏ธโƒฃ Testing protected route...'); + const protectedResponse = await fetch(`${BASE_URL}/rbac-test/user-only`, { + headers: { 'Authorization': `Bearer ${loginData.accessToken}` } + }); + + if (protectedResponse.ok) { + const protectedData = await protectedResponse.json(); + console.log('โœ… Protected route accessed:', protectedData.message); + } + + // Test 5: Token Refresh + console.log('\n5๏ธโƒฃ Testing token refresh...'); + const refreshResponse = await fetch(`${BASE_URL}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: loginData.refreshToken }) + }); + + if (refreshResponse.ok) { + const refreshData = await refreshResponse.json(); + console.log('โœ… Token refresh successful'); + console.log('๐Ÿ“ New Access Token:', refreshData.accessToken.substring(0, 30) + '...'); + } + + // Test 6: Logout + console.log('\n6๏ธโƒฃ Testing logout...'); + const logoutResponse = await fetch(`${BASE_URL}/auth/logout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: loginData.refreshToken }) + }); + + if (logoutResponse.ok) { + console.log('โœ… Logout successful'); + } + + console.log('\n๐ŸŽ‰ All tests completed successfully!'); + } else { + console.log('โŒ Login failed:', loginData.message); + } +} + +testAPI().catch(console.error); +``` + +## ๐Ÿ› Troubleshooting + +### Common Issues: + +1. **MongoDB Connection Error** + - Ensure MongoDB is running + - Check MONGO_URI in .env file + +2. **JWT Secret Error** + - Ensure JWT_SECRET and REFRESH_TOKEN_SECRET are set in .env + +3. **Port Already in Use** + - Change PORT in .env file + - Kill existing process: `lsof -ti:5000 | xargs kill -9` + +4. **Token Validation Errors** + - Check if tokens are properly formatted + - Ensure Authorization header includes "Bearer " prefix + +### Debug Mode: +Add this to your .env file for detailed logging: +```env +NODE_ENV=development +DEBUG=true +``` + +## ๐Ÿ“Š Expected Results + +โœ… **All endpoints should return proper responses** +โœ… **Authentication flow should work seamlessly** +โœ… **Refresh token mechanism should function correctly** +โœ… **Logout should invalidate tokens** +โœ… **Protected routes should require valid tokens** +โœ… **Role-based access should work as expected** + +## ๐ŸŽฏ Success Criteria + +- [ ] Server starts without errors +- [ ] Database seeding completes successfully +- [ ] User registration works +- [ ] Login returns both access and refresh tokens +- [ ] Protected routes require authentication +- [ ] Token refresh works correctly +- [ ] Logout invalidates refresh token +- [ ] All error cases are handled properly + +Happy Testing! ๐Ÿš€ diff --git a/setup-and-test.js b/setup-and-test.js new file mode 100644 index 0000000..3c25fdf --- /dev/null +++ b/setup-and-test.js @@ -0,0 +1,100 @@ +/** + * Setup and Test Script for RBAC Project + * This script helps you set up and test the complete RBAC functionality + */ + +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +async function setupAndTest() { + console.log('๐Ÿš€ RBAC Project Setup and Test\n'); + + try { + // Step 1: Check if .env file exists + console.log('1๏ธโƒฃ Checking environment setup...'); + try { + const envCheck = await execAsync('ls .env 2>/dev/null || echo "not found"'); + if (envCheck.stdout.includes('not found')) { + console.log('โš ๏ธ .env file not found. Please create it with the following content:'); + console.log(` +PORT=5000 +MONGO_URI=mongodb://localhost:27017/rbac +JWT_SECRET=your-super-secret-jwt-key-here +JWT_EXPIRY=1d +REFRESH_TOKEN_SECRET=your-super-secret-refresh-token-key-here +REFRESH_TOKEN_EXPIRY=7d +CORS_URL=http://localhost:3000 + `); + return; + } else { + console.log('โœ… .env file found'); + } + } catch (error) { + console.log('โš ๏ธ Could not check .env file'); + } + + // Step 2: Check if node_modules exists + console.log('\n2๏ธโƒฃ Checking dependencies...'); + try { + const depsCheck = await execAsync('ls node_modules 2>/dev/null || echo "not found"'); + if (depsCheck.stdout.includes('not found')) { + console.log('๐Ÿ“ฆ Installing dependencies...'); + await execAsync('npm install'); + console.log('โœ… Dependencies installed'); + } else { + console.log('โœ… Dependencies already installed'); + } + } catch (error) { + console.log('โŒ Error checking dependencies:', error.message); + } + + // Step 3: Check MongoDB connection + console.log('\n3๏ธโƒฃ Checking MongoDB connection...'); + try { + await execAsync('mongosh --eval "db.runCommand({ping: 1})" --quiet'); + console.log('โœ… MongoDB is running'); + } catch (error) { + console.log('โŒ MongoDB connection failed'); + console.log('๐Ÿ’ก Please start MongoDB: mongod'); + return; + } + + // Step 4: Seed the database + console.log('\n4๏ธโƒฃ Seeding database...'); + try { + await execAsync('node src/seed/seedRoles.js'); + console.log('โœ… Database seeded successfully'); + } catch (error) { + console.log('โŒ Database seeding failed:', error.message); + return; + } + + // Step 5: Start the server + console.log('\n5๏ธโƒฃ Starting the server...'); + console.log('๐Ÿš€ Run this command in a new terminal: npm run dev'); + console.log('โณ Wait for "Server is running at port : 5000" message'); + console.log('๐Ÿงช Then run: node test-api.js'); + + console.log('\n๐Ÿ“‹ Manual Testing Steps:'); + console.log('1. Open a new terminal'); + console.log('2. Run: npm run dev'); + console.log('3. Wait for server to start'); + console.log('4. Run: node test-api.js'); + console.log('5. Check all tests pass'); + + console.log('\n๐Ÿ”— API Endpoints to test manually:'); + console.log('โ€ข Health: GET http://localhost:5000/'); + console.log('โ€ข Register: POST http://localhost:5000/api/auth/register'); + console.log('โ€ข Login: POST http://localhost:5000/api/auth/login'); + console.log('โ€ข Refresh: POST http://localhost:5000/api/auth/refresh'); + console.log('โ€ข Logout: POST http://localhost:5000/api/auth/logout'); + console.log('โ€ข Protected: GET http://localhost:5000/api/rbac-test/user-only'); + + } catch (error) { + console.error('โŒ Setup failed:', error.message); + } +} + +setupAndTest(); diff --git a/src/controllers/authController.js b/src/controllers/authController.js index 81ca5be..2b2a487 100644 --- a/src/controllers/authController.js +++ b/src/controllers/authController.js @@ -1,4 +1,4 @@ -import { registerUserService,loginUserService } from '../services/authService.js'; +import { registerUserService, loginUserService, refreshTokenService, logoutService } from '../services/authService.js'; export const registerUser = async (req, res) => { try { @@ -22,7 +22,8 @@ export const loginUser = async (req, res) => { return res.status(200).json({ success: true, message: 'Login successful', - token: result.accessToken, + accessToken: result.accessToken, + refreshToken: result.refreshToken, user: result.user, }); } catch (error) { @@ -30,4 +31,44 @@ export const loginUser = async (req, res) => { const status = error.statusCode || 400; return res.status(status).json({ success: false, message: error.message || 'Login failed' }); } +}; + +export const refreshToken = async (req, res) => { + try { + const { refreshToken } = req.body; + const result = await refreshTokenService(refreshToken); + + return res.status(200).json({ + success: true, + message: 'Token refreshed successfully', + accessToken: result.accessToken, + user: result.user, + }); + } catch (error) { + console.error('Error in refreshToken:', error); + const status = error.statusCode || 401; + return res.status(status).json({ + success: false, + message: error.message || 'Token refresh failed' + }); + } +}; + +export const logout = async (req, res) => { + try { + const { refreshToken } = req.body; + const result = await logoutService(refreshToken); + + return res.status(200).json({ + success: true, + message: result.message, + }); + } catch (error) { + console.error('Error in logout:', error); + const status = error.statusCode || 400; + return res.status(status).json({ + success: false, + message: error.message || 'Logout failed' + }); + } }; \ No newline at end of file diff --git a/src/models/user.model.js b/src/models/user.model.js index 0b150be..af7c9c7 100644 --- a/src/models/user.model.js +++ b/src/models/user.model.js @@ -31,6 +31,9 @@ const userschema=new Schema({ refreshToken:{ type:String }, + refreshTokenExpiry:{ + type:Date + }, role: { type: mongoose.Schema.Types.ObjectId, ref: 'Role', @@ -96,5 +99,41 @@ userschema.methods.refreshAccessToken = function () { } }; +userschema.methods.generateRefreshToken = function () { + try { + if (!process.env.REFRESH_TOKEN_SECRET) { + console.error('REFRESH_TOKEN_SECRET not set in environment variables'); + throw new Error('REFRESH_TOKEN_SECRET not set in environment'); + } + + const refreshToken = jwt.sign( + { + _id: this._id, + type: 'refresh' + }, + process.env.REFRESH_TOKEN_SECRET, + { + expiresIn: process.env.REFRESH_TOKEN_EXPIRY || '7d', + } + ); + + // Set expiry date for database storage + const expiryDate = new Date(); + expiryDate.setDate(expiryDate.getDate() + 7); // 7 days from now + + return { refreshToken, expiryDate }; + } + catch (error) { + console.error('Refresh token generation error:', error.message); + throw new Error(`Failed to generate refresh token: ${error.message}`); + } +}; + +userschema.methods.clearRefreshToken = function () { + this.refreshToken = undefined; + this.refreshTokenExpiry = undefined; + return this.save(); +}; + export const User=mongoose.model('User',userschema); \ No newline at end of file diff --git a/src/routes/authRoutes.js b/src/routes/authRoutes.js index 16b86df..c5e7075 100644 --- a/src/routes/authRoutes.js +++ b/src/routes/authRoutes.js @@ -1,9 +1,11 @@ import express from 'express'; -import { registerUser,loginUser } from '../controllers/authController.js'; +import { registerUser, loginUser, refreshToken, logout } from '../controllers/authController.js'; const router = express.Router(); router.post('/register', registerUser); router.post('/login', loginUser); +router.post('/refresh', refreshToken); +router.post('/logout', logout); export default router; diff --git a/src/seed/seedRoles.js b/src/seed/seedRoles.js index 12a6f25..13b00a6 100644 --- a/src/seed/seedRoles.js +++ b/src/seed/seedRoles.js @@ -1,7 +1,7 @@ import mongoose from 'mongoose'; import dotenv from 'dotenv'; -import Role from '../models/role.model.js'; -import Permission from '../models/permission.model.js'; +import Role from '../models/Role.model.js'; +import Permission from '../models/Permission.model.js'; dotenv.config(); diff --git a/src/services/authService.js b/src/services/authService.js index 3f462dc..11bcd36 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,5 +1,5 @@ import { User } from '../models/user.model.js'; -import Role from '../models/role.model.js'; +import Role from '../models/Role.model.js'; import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; @@ -74,8 +74,26 @@ export const loginUserService = async ({ email, password }) => { }; const accessToken = signAccessToken(payload); + + // Generate refresh token + let refreshToken, expiryDate; + try { + const tokenData = user.generateRefreshToken(); + refreshToken = tokenData.refreshToken; + expiryDate = tokenData.expiryDate; + } catch (error) { + console.error('Error generating refresh token:', error.message); + throw new Error(`Refresh token generation failed: ${error.message}`); + } + + // Save refresh token to database + user.refreshToken = refreshToken; + user.refreshTokenExpiry = expiryDate; + await user.save(); + return { accessToken, + refreshToken, user: { id: user._id, username: user.username, @@ -84,4 +102,85 @@ export const loginUserService = async ({ email, password }) => { role: roleName, }, }; +}; + +export const refreshTokenService = async (refreshToken) => { + if (!refreshToken) { + const err = new Error('Refresh token is required'); + err.statusCode = 400; + throw err; + } + + try { + // Verify the refresh token + const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET); + + // Find user with the refresh token + const user = await User.findOne({ + _id: decoded._id, + refreshToken: refreshToken + }).populate('role'); + + if (!user) { + const err = new Error('Invalid refresh token'); + err.statusCode = 401; + throw err; + } + + // Check if refresh token is expired + if (user.refreshTokenExpiry && new Date() > user.refreshTokenExpiry) { + const err = new Error('Refresh token expired'); + err.statusCode = 401; + throw err; + } + + // Generate new access token + const roleName = user.role && user.role.name ? user.role.name : null; + const payload = { + id: user._id, + role: roleName, + }; + + const newAccessToken = signAccessToken(payload); + + return { + accessToken: newAccessToken, + user: { + id: user._id, + username: user.username, + email: user.email, + fullname: user.fullname, + role: roleName, + }, + }; + } catch (error) { + if (error.name === 'JsonWebTokenError' || error.name === 'TokenExpiredError') { + const err = new Error('Invalid or expired refresh token'); + err.statusCode = 401; + throw err; + } + throw error; + } +}; + +export const logoutService = async (refreshToken) => { + if (!refreshToken) { + const err = new Error('Refresh token is required'); + err.statusCode = 400; + throw err; + } + + try { + // Find user with the refresh token and clear it + const user = await User.findOne({ refreshToken: refreshToken }); + + if (user) { + await user.clearRefreshToken(); + } + + return { message: 'Logged out successfully' }; + } catch (error) { + console.error('Error in logoutService:', error); + throw new Error('Logout failed'); + } }; \ No newline at end of file diff --git a/test-api.js b/test-api.js new file mode 100644 index 0000000..ad2ebe0 --- /dev/null +++ b/test-api.js @@ -0,0 +1,152 @@ +/** + * Simple API Test Script for RBAC Project + * Run this script to test all functionality + */ + +const BASE_URL = 'http://localhost:5000/api'; + +async function testAPI() { + console.log('๐Ÿงช Starting RBAC API Tests...\n'); + + try { + // Test 1: Health Check + console.log('1๏ธโƒฃ Testing server health...'); + const health = await fetch('http://localhost:5000/'); + const healthText = await health.text(); + console.log('โœ… Server response:', healthText); + + // Test 2: Register User + console.log('\n2๏ธโƒฃ Testing user registration...'); + const registerResponse = await fetch(`${BASE_URL}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + username: 'testuser', + email: 'test@example.com', + fullname: 'Test User', + password: 'password123' + }) + }); + + const registerData = await registerResponse.json(); + if (registerData.success) { + console.log('โœ… User registration successful'); + } else { + console.log('โš ๏ธ Registration response:', registerData.message); + } + + // Test 3: Login + console.log('\n3๏ธโƒฃ Testing user login...'); + const loginResponse = await fetch(`${BASE_URL}/auth/login`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + email: 'test@example.com', + password: 'password123' + }) + }); + + const loginData = await loginResponse.json(); + if (loginData.success) { + console.log('โœ… Login successful'); + console.log('๐Ÿ“ Access Token:', loginData.accessToken.substring(0, 30) + '...'); + console.log('๐Ÿ”„ Refresh Token:', loginData.refreshToken.substring(0, 30) + '...'); + + // Test 4: Protected Route + console.log('\n4๏ธโƒฃ Testing protected route...'); + const protectedResponse = await fetch(`${BASE_URL}/rbac-test/user-only`, { + headers: { 'Authorization': `Bearer ${loginData.accessToken}` } + }); + + if (protectedResponse.ok) { + const protectedData = await protectedResponse.json(); + console.log('โœ… Protected route accessed:', protectedData.message); + } else { + console.log('โŒ Protected route failed:', protectedResponse.statusText); + } + + // Test 5: Token Refresh + console.log('\n5๏ธโƒฃ Testing token refresh...'); + const refreshResponse = await fetch(`${BASE_URL}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: loginData.refreshToken }) + }); + + const refreshData = await refreshResponse.json(); + if (refreshData.success) { + console.log('โœ… Token refresh successful'); + console.log('๐Ÿ“ New Access Token:', refreshData.accessToken.substring(0, 30) + '...'); + + // Test 6: Use new token + console.log('\n6๏ธโƒฃ Testing with new access token...'); + const newProtectedResponse = await fetch(`${BASE_URL}/rbac-test/user-only`, { + headers: { 'Authorization': `Bearer ${refreshData.accessToken}` } + }); + + if (newProtectedResponse.ok) { + const newProtectedData = await newProtectedResponse.json(); + console.log('โœ… New token works:', newProtectedData.message); + } + } else { + console.log('โŒ Token refresh failed:', refreshData.message); + } + + // Test 7: Logout + console.log('\n7๏ธโƒฃ Testing logout...'); + const logoutResponse = await fetch(`${BASE_URL}/auth/logout`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: loginData.refreshToken }) + }); + + const logoutData = await logoutResponse.json(); + if (logoutData.success) { + console.log('โœ… Logout successful:', logoutData.message); + } else { + console.log('โŒ Logout failed:', logoutData.message); + } + + // Test 8: Try refresh after logout (should fail) + console.log('\n8๏ธโƒฃ Testing refresh after logout (should fail)...'); + const invalidRefreshResponse = await fetch(`${BASE_URL}/auth/refresh`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ refreshToken: loginData.refreshToken }) + }); + + const invalidRefreshData = await invalidRefreshResponse.json(); + if (!invalidRefreshData.success) { + console.log('โœ… Refresh correctly blocked after logout'); + } else { + console.log('โŒ Refresh should have been blocked'); + } + + console.log('\n๐ŸŽ‰ All tests completed successfully!'); + console.log('\n๐Ÿ“‹ Test Summary:'); + console.log('โœ… Server health check'); + console.log('โœ… User registration'); + console.log('โœ… User login with tokens'); + console.log('โœ… Protected route access'); + console.log('โœ… Token refresh mechanism'); + console.log('โœ… New token usage'); + console.log('โœ… Logout functionality'); + console.log('โœ… Token invalidation'); + + } else { + console.log('โŒ Login failed:', loginData.message); + console.log('๐Ÿ’ก Make sure the server is running and database is seeded'); + } + + } catch (error) { + console.error('โŒ Test failed with error:', error.message); + console.log('\n๐Ÿ”ง Troubleshooting:'); + console.log('1. Make sure the server is running: npm run dev'); + console.log('2. Check if MongoDB is running'); + console.log('3. Verify .env file has correct values'); + console.log('4. Run database seeding: node src/seed/seedRoles.js'); + } +} + +// Run the test +testAPI();