Implement hybrid Azure AD SSO + Password authentication system
✅ Backend Implementation: - Add Azure AD JWT token validation middleware - Create hybrid authentication system supporting both Azure AD and password auth - Implement auto-provisioning for new Azure AD users - Add admin controls to toggle password authentication - Update all API routes to use hybrid authentication - Add database fields for authentication (password, lastLoginAt) - Create comprehensive auth routes with validation endpoints ✅ Frontend Implementation: - Install and configure Azure MSAL browser library - Create Azure AD authentication service with popup/redirect support - Build hybrid authentication service managing both auth methods - Update Login.vue with modern dual-authentication UI - Implement dynamic password auth toggle based on admin settings - Update App.vue for proper session management and validation - Modify API service to handle both token types ✅ Security Features: - Azure AD tenant validation (Oliver Agency) - Role-based access control with auto-admin assignment - JWT token validation for both auth methods - Automatic user provisioning with proper defaults - Session validation and automatic logout on token expiry ✅ Admin Features: - Toggle password authentication on/off - Manage users from both authentication methods - Full role and agent access control - Azure AD user auto-provisioning as regular users ✅ Configuration: - Azure AD: Tenant e519c2e6-bc6d-4fdf-8d9c-923c2f002385 - Client ID: 9079054c-9620-4757-a256-23413042f1ef - Development redirect URI support - Fallback password authentication for testing 🔧 Technical Stack: - Azure MSAL Browser & Node libraries - JWT token validation and hybrid middleware - Database schema updates with migrations - Vue.js integration with MSAL - Express.js hybrid authentication routes 🚀 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
574e390be1
commit
013f57fe60
21 changed files with 2186 additions and 102 deletions
526
AUTHENTICATION_GUIDE.md
Normal file
526
AUTHENTICATION_GUIDE.md
Normal file
|
|
@ -0,0 +1,526 @@
|
|||
# Authentication Implementation Guide
|
||||
|
||||
## Overview
|
||||
This guide provides a comprehensive approach to implementing authentication in web applications, covering frontend login components, backend authentication, route protection, and session management.
|
||||
|
||||
## Frontend Implementation
|
||||
|
||||
### 1. Login Component Structure
|
||||
|
||||
```jsx
|
||||
// components/LoginForm.jsx
|
||||
import React, { useState } from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const LoginForm = () => {
|
||||
const [credentials, setCredentials] = useState({ username: '', password: '' });
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const { login } = useAuth();
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setError('');
|
||||
|
||||
try {
|
||||
await login(credentials);
|
||||
} catch (err) {
|
||||
setError(err.message || 'Login failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="login-container">
|
||||
<form onSubmit={handleSubmit} className="login-form">
|
||||
<h2>Login</h2>
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Username"
|
||||
value={credentials.username}
|
||||
onChange={(e) => setCredentials({...credentials, username: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<input
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={credentials.password}
|
||||
onChange={(e) => setCredentials({...credentials, password: e.target.value})}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Logging in...' : 'Login'}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
```
|
||||
|
||||
### 2. Authentication Context
|
||||
|
||||
```jsx
|
||||
// context/AuthContext.jsx
|
||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { authAPI } from '../services/authService';
|
||||
|
||||
const AuthContext = createContext();
|
||||
|
||||
export const useAuth = () => {
|
||||
const context = useContext(AuthContext);
|
||||
if (!context) {
|
||||
throw new Error('useAuth must be used within AuthProvider');
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const AuthProvider = ({ children }) => {
|
||||
const [user, setUser] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [token, setToken] = useState(localStorage.getItem('token'));
|
||||
|
||||
useEffect(() => {
|
||||
if (token) {
|
||||
validateToken();
|
||||
} else {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [token]);
|
||||
|
||||
const validateToken = async () => {
|
||||
try {
|
||||
const userData = await authAPI.validateToken(token);
|
||||
setUser(userData);
|
||||
} catch (error) {
|
||||
logout();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const login = async (credentials) => {
|
||||
const response = await authAPI.login(credentials);
|
||||
const { user: userData, token: authToken } = response;
|
||||
|
||||
setUser(userData);
|
||||
setToken(authToken);
|
||||
localStorage.setItem('token', authToken);
|
||||
};
|
||||
|
||||
const logout = () => {
|
||||
setUser(null);
|
||||
setToken(null);
|
||||
localStorage.removeItem('token');
|
||||
};
|
||||
|
||||
const value = {
|
||||
user,
|
||||
login,
|
||||
logout,
|
||||
loading,
|
||||
isAuthenticated: !!user
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={value}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Protected Route Component
|
||||
|
||||
```jsx
|
||||
// components/ProtectedRoute.jsx
|
||||
import React from 'react';
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
import LoginForm from './LoginForm';
|
||||
import LoadingSpinner from './LoadingSpinner';
|
||||
|
||||
const ProtectedRoute = ({ children }) => {
|
||||
const { isAuthenticated, loading } = useAuth();
|
||||
|
||||
if (loading) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <LoginForm />;
|
||||
}
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default ProtectedRoute;
|
||||
```
|
||||
|
||||
### 4. App Structure with Authentication
|
||||
|
||||
```jsx
|
||||
// App.jsx
|
||||
import React from 'react';
|
||||
import { AuthProvider } from './context/AuthContext';
|
||||
import ProtectedRoute from './components/ProtectedRoute';
|
||||
import MainApp from './components/MainApp';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<AuthProvider>
|
||||
<div className="App">
|
||||
<ProtectedRoute>
|
||||
<MainApp />
|
||||
</ProtectedRoute>
|
||||
</div>
|
||||
</AuthProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
## Backend Implementation
|
||||
|
||||
### 5. Authentication Service
|
||||
|
||||
```javascript
|
||||
// services/authService.js
|
||||
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
export const authAPI = {
|
||||
login: async (credentials) => {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Login failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
validateToken: async (token) => {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/validate`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token validation failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
},
|
||||
|
||||
logout: async (token) => {
|
||||
await fetch(`${API_BASE_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### 6. API Interceptor for Authenticated Requests
|
||||
|
||||
```javascript
|
||||
// utils/apiClient.js
|
||||
import { useAuth } from '../context/AuthContext';
|
||||
|
||||
const createAPIClient = () => {
|
||||
const baseURL = process.env.REACT_APP_API_URL || 'http://localhost:3001/api';
|
||||
|
||||
const apiClient = async (endpoint, options = {}) => {
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
const response = await fetch(`${baseURL}${endpoint}`, config);
|
||||
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
};
|
||||
|
||||
return apiClient;
|
||||
};
|
||||
|
||||
export default createAPIClient();
|
||||
```
|
||||
|
||||
## Backend Server Implementation (Node.js/Express)
|
||||
|
||||
### 7. Authentication Middleware
|
||||
|
||||
```javascript
|
||||
// middleware/auth.js
|
||||
const jwt = require('jsonwebtoken');
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access token required' });
|
||||
}
|
||||
|
||||
jwt.verify(token, JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({ message: 'Invalid or expired token' });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = { authenticateToken };
|
||||
```
|
||||
|
||||
### 8. Auth Routes
|
||||
|
||||
```javascript
|
||||
// routes/auth.js
|
||||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
||||
|
||||
// Mock user database (replace with your database)
|
||||
const users = [
|
||||
{
|
||||
id: 1,
|
||||
username: 'admin',
|
||||
password: '$2b$10$hash', // bcrypt hash of 'password'
|
||||
email: 'admin@example.com'
|
||||
}
|
||||
];
|
||||
|
||||
// Login endpoint
|
||||
router.post('/login', async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
|
||||
try {
|
||||
const user = users.find(u => u.username === username);
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
const token = jwt.sign(
|
||||
{ id: user.id, username: user.username },
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
user: { id: user.id, username: user.username, email: user.email },
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
res.status(500).json({ message: 'Server error' });
|
||||
}
|
||||
});
|
||||
|
||||
// Token validation endpoint
|
||||
router.get('/validate', authenticateToken, (req, res) => {
|
||||
const user = users.find(u => u.id === req.user.id);
|
||||
if (!user) {
|
||||
return res.status(404).json({ message: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email
|
||||
});
|
||||
});
|
||||
|
||||
// Logout endpoint
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// In a real application, you might want to blacklist the token
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
```
|
||||
|
||||
## CSS Styles
|
||||
|
||||
### 9. Login Form Styles
|
||||
|
||||
```css
|
||||
/* styles/login.css */
|
||||
.login-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
background: white;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.login-form h2 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-group input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.form-group input:focus {
|
||||
outline: none;
|
||||
border-color: #007bff;
|
||||
}
|
||||
|
||||
.login-form button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.login-form button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
.login-form button:disabled {
|
||||
background-color: #6c757d;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
padding: 0.75rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
### Frontend Setup
|
||||
- [ ] Create AuthContext for state management
|
||||
- [ ] Implement LoginForm component
|
||||
- [ ] Add ProtectedRoute wrapper
|
||||
- [ ] Set up API client with token management
|
||||
- [ ] Handle token persistence in localStorage
|
||||
- [ ] Add loading states and error handling
|
||||
|
||||
### Backend Setup
|
||||
- [ ] Create authentication middleware
|
||||
- [ ] Implement login/validate/logout endpoints
|
||||
- [ ] Set up JWT token generation and verification
|
||||
- [ ] Add password hashing (bcrypt)
|
||||
- [ ] Secure routes with authentication middleware
|
||||
|
||||
### Security Considerations
|
||||
- [ ] Use HTTPS in production
|
||||
- [ ] Implement proper CORS policies
|
||||
- [ ] Add rate limiting to login endpoints
|
||||
- [ ] Use secure JWT secrets
|
||||
- [ ] Implement token refresh mechanism
|
||||
- [ ] Add logout functionality that invalidates tokens
|
||||
|
||||
### Testing
|
||||
- [ ] Test login/logout flow
|
||||
- [ ] Verify protected routes work correctly
|
||||
- [ ] Test token expiration handling
|
||||
- [ ] Validate error states and user feedback
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Frontend (.env)
|
||||
REACT_APP_API_URL=http://localhost:3001/api
|
||||
|
||||
# Backend (.env)
|
||||
JWT_SECRET=your-super-secure-secret-key
|
||||
PORT=3001
|
||||
DB_CONNECTION_STRING=your-database-url
|
||||
```
|
||||
|
||||
## Usage Instructions
|
||||
|
||||
1. **Setup**: Wrap your app with `AuthProvider`
|
||||
2. **Protection**: Wrap protected content with `ProtectedRoute`
|
||||
3. **Authentication**: Use `useAuth()` hook to access auth state
|
||||
4. **API Calls**: Use the configured API client for authenticated requests
|
||||
|
||||
This guide provides a complete authentication system that can be adapted to any React application with a Node.js backend.
|
||||
22
admin/package-lock.json
generated
22
admin/package-lock.json
generated
|
|
@ -9,6 +9,7 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.22.0",
|
||||
"axios": "^1.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
|
|
@ -22,6 +23,27 @@
|
|||
"vite": "^5.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-browser": {
|
||||
"version": "4.22.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-4.22.0.tgz",
|
||||
"integrity": "sha512-JLWHzAW1aZ/L190Th56jN+2t3T1dMvXOs1obXYLEr3ZWi81vVmBCt0di3mPvTTOiWoE0Cf/4hVQ/LINilqjObA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.12.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "15.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.12.0.tgz",
|
||||
"integrity": "sha512-4ucXbjVw8KJ5QBgnGJUeA07c8iznwlk5ioHIhI4ASXcXgcf2yRFhWzYOyWg/cI49LC9ekpFJeQtO3zjDTbl6TQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@azure/msal-browser": "^4.22.0",
|
||||
"axios": "^1.6.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"highlight.js": "^11.9.0",
|
||||
|
|
|
|||
|
|
@ -18,11 +18,11 @@
|
|||
<span class="user-avatar">{{ currentUser?.role === 'admin' ? '👑' : '👤' }}</span>
|
||||
<div class="user-details">
|
||||
<span class="user-name">{{ currentUser?.name }}</span>
|
||||
<span v-if="currentUser?.allowedAgents" class="user-access-info">
|
||||
<span v-if="currentUser?.allowedAgents && currentUser.allowedAgents.length > 0" class="user-access-info">
|
||||
Limited Access ({{ currentUser.allowedAgents.length }} agents)
|
||||
</span>
|
||||
<span v-else-if="currentUser?.role !== 'admin'" class="user-access-info">
|
||||
Full Access
|
||||
{{ currentUser?.allowedAgents && currentUser.allowedAgents.length === 0 ? 'No Access' : 'Full Access' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -31,45 +31,68 @@
|
|||
</nav>
|
||||
|
||||
<main class="main-content" :class="{ 'no-nav': !isLoggedIn }">
|
||||
<router-view />
|
||||
<div v-if="loading" class="loading-screen">
|
||||
<div class="loading-spinner">
|
||||
<div class="spinner"></div>
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
<router-view v-else />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import hybridAuthService from './services/hybridAuthService'
|
||||
|
||||
export default {
|
||||
name: 'App',
|
||||
data() {
|
||||
return {
|
||||
currentUser: null
|
||||
currentUser: null,
|
||||
loading: true
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isLoggedIn() {
|
||||
return !!this.currentUser
|
||||
return !!this.currentUser && hybridAuthService.isAuthenticated()
|
||||
},
|
||||
isAdmin() {
|
||||
return this.currentUser?.role === 'admin'
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.loadCurrentUser()
|
||||
// Listen for storage changes to update user state
|
||||
window.addEventListener('storage', this.handleStorageChange)
|
||||
},
|
||||
beforeUnmount() {
|
||||
window.removeEventListener('storage', this.handleStorageChange)
|
||||
async mounted() {
|
||||
await this.initializeAuth()
|
||||
},
|
||||
methods: {
|
||||
loadCurrentUser() {
|
||||
const currentUser = localStorage.getItem('currentUser')
|
||||
this.currentUser = currentUser ? JSON.parse(currentUser) : null
|
||||
async initializeAuth() {
|
||||
try {
|
||||
// Initialize hybrid auth service
|
||||
await hybridAuthService.initialize()
|
||||
|
||||
if (hybridAuthService.isAuthenticated()) {
|
||||
// Validate the existing session
|
||||
const isValid = await hybridAuthService.validateSession()
|
||||
if (isValid) {
|
||||
this.currentUser = hybridAuthService.getCurrentUser()
|
||||
} else {
|
||||
this.currentUser = null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Authentication initialization failed:', error)
|
||||
this.currentUser = null
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
handleStorageChange() {
|
||||
this.loadCurrentUser()
|
||||
},
|
||||
logout() {
|
||||
localStorage.removeItem('currentUser')
|
||||
async logout() {
|
||||
try {
|
||||
await hybridAuthService.logout()
|
||||
} catch (error) {
|
||||
console.warn('Logout error:', error)
|
||||
}
|
||||
|
||||
this.currentUser = null
|
||||
this.$router.push('/login')
|
||||
}
|
||||
|
|
@ -213,6 +236,37 @@ export default {
|
|||
height: 100vh;
|
||||
}
|
||||
|
||||
.loading-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
border: 3px solid #f3f3f3;
|
||||
border-top: 3px solid #ffc407;
|
||||
border-radius: 50%;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 1rem;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-spinner p {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import Home from './pages/Home.vue'
|
|||
import Chat from './pages/Chat.vue'
|
||||
import Login from './pages/Login.vue'
|
||||
import Admin from './pages/Admin.vue'
|
||||
import hybridAuthService from './services/hybridAuthService'
|
||||
import './style.css'
|
||||
|
||||
const routes = [
|
||||
|
|
@ -20,21 +21,25 @@ const router = createRouter({
|
|||
})
|
||||
|
||||
// Authentication guard
|
||||
router.beforeEach((to, from, next) => {
|
||||
const currentUser = localStorage.getItem('currentUser')
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth
|
||||
const requiresAdmin = to.meta.requiresAdmin
|
||||
const isAuthenticated = hybridAuthService.isAuthenticated()
|
||||
|
||||
if (requiresAuth && !currentUser) {
|
||||
if (requiresAuth && !isAuthenticated) {
|
||||
next('/login')
|
||||
} else if (to.path === '/login' && currentUser) {
|
||||
} else if (to.path === '/login' && isAuthenticated) {
|
||||
next('/chat')
|
||||
} else if (requiresAdmin && currentUser) {
|
||||
const user = JSON.parse(currentUser)
|
||||
if (user.role !== 'admin') {
|
||||
next('/chat') // Redirect non-admin users to chat
|
||||
} else {
|
||||
next()
|
||||
} else if (requiresAdmin && isAuthenticated) {
|
||||
try {
|
||||
const user = hybridAuthService.getCurrentUser()
|
||||
if (!user || user.role !== 'admin') {
|
||||
next('/chat') // Redirect non-admin users to chat
|
||||
} else {
|
||||
next()
|
||||
}
|
||||
} catch (error) {
|
||||
next('/login')
|
||||
}
|
||||
} else {
|
||||
next()
|
||||
|
|
|
|||
|
|
@ -4,32 +4,88 @@
|
|||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<h1>Ideas Generator 2025</h1>
|
||||
<p>Choose your account to continue</p>
|
||||
<p>Choose your sign-in method</p>
|
||||
</div>
|
||||
|
||||
<div class="login-form">
|
||||
<div class="user-options">
|
||||
<button
|
||||
@click="loginAs('admin@oliver.agency')"
|
||||
class="user-button admin-button"
|
||||
>
|
||||
<div class="user-icon">👑</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">Administrator</div>
|
||||
<div class="user-email">admin@oliver.agency</div>
|
||||
</div>
|
||||
</button>
|
||||
<!-- Azure AD Login (Primary) -->
|
||||
<div class="auth-section">
|
||||
<h3>Sign in with Azure AD</h3>
|
||||
<div v-if="error && error.includes('Azure')" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="loginWithAzure"
|
||||
:disabled="loading"
|
||||
class="azure-login-button"
|
||||
>
|
||||
<div class="azure-icon">🏢</div>
|
||||
<div class="azure-text">
|
||||
<div class="azure-title">{{ loading && authMethod === 'azure' ? 'Signing in...' : 'Sign in with Oliver Agency' }}</div>
|
||||
<div class="azure-subtitle">Use your work account</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Password Login (Fallback) -->
|
||||
<div v-if="passwordAuthEnabled" class="auth-section">
|
||||
<div class="auth-divider">
|
||||
<span>or</span>
|
||||
</div>
|
||||
|
||||
<h3>Sign in with Password</h3>
|
||||
<div v-if="error && !error.includes('Azure')" class="error-message">
|
||||
{{ error }}
|
||||
</div>
|
||||
|
||||
<form @submit.prevent="loginWithPassword" class="login-form">
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="credentials.email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
required
|
||||
:disabled="loading"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<input
|
||||
v-model="credentials.password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
:disabled="loading"
|
||||
class="form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@click="loginAs('user@oliver.agency')"
|
||||
class="user-button user-button"
|
||||
type="submit"
|
||||
:disabled="loading"
|
||||
class="login-button"
|
||||
>
|
||||
<div class="user-icon">👤</div>
|
||||
<div class="user-info">
|
||||
<div class="user-name">User</div>
|
||||
<div class="user-email">user@oliver.agency</div>
|
||||
</div>
|
||||
{{ loading && authMethod === 'password' ? 'Signing in...' : 'Sign In with Password' }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<div class="login-help">
|
||||
<p class="demo-info">
|
||||
<strong>Demo Account:</strong><br>
|
||||
Email: daveporter@oliver.agency<br>
|
||||
Password: changeMe123!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Disabled Message -->
|
||||
<div v-else class="auth-section">
|
||||
<div class="auth-divider">
|
||||
<span>or</span>
|
||||
</div>
|
||||
<div class="disabled-message">
|
||||
Password authentication is currently disabled by administrators.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -38,22 +94,67 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import hybridAuthService from '../services/hybridAuthService'
|
||||
|
||||
export default {
|
||||
name: 'Login',
|
||||
methods: {
|
||||
loginAs(email) {
|
||||
// Store user info in localStorage
|
||||
const userInfo = {
|
||||
email,
|
||||
name: email === 'admin@oliver.agency' ? 'Administrator' : 'User',
|
||||
role: email === 'admin@oliver.agency' ? 'admin' : 'user',
|
||||
loginTime: new Date().toISOString()
|
||||
}
|
||||
|
||||
localStorage.setItem('currentUser', JSON.stringify(userInfo))
|
||||
|
||||
// Redirect to chat page
|
||||
data() {
|
||||
return {
|
||||
credentials: {
|
||||
email: '',
|
||||
password: ''
|
||||
},
|
||||
loading: false,
|
||||
error: '',
|
||||
authMethod: '',
|
||||
passwordAuthEnabled: true
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
// Initialize hybrid auth service
|
||||
await hybridAuthService.initialize()
|
||||
|
||||
// Check if already authenticated
|
||||
if (hybridAuthService.isAuthenticated()) {
|
||||
this.$router.push('/chat')
|
||||
return
|
||||
}
|
||||
|
||||
// Check password authentication setting
|
||||
this.passwordAuthEnabled = hybridAuthService.isPasswordAuthEnabled()
|
||||
},
|
||||
methods: {
|
||||
async loginWithAzure() {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
this.authMethod = 'azure'
|
||||
|
||||
try {
|
||||
await hybridAuthService.loginWithAzure(false) // Use popup, not redirect
|
||||
this.$router.push('/chat')
|
||||
} catch (error) {
|
||||
console.error('Azure login error:', error)
|
||||
this.error = 'Azure login failed: ' + (error.message || 'Unknown error')
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.authMethod = ''
|
||||
}
|
||||
},
|
||||
|
||||
async loginWithPassword() {
|
||||
this.loading = true
|
||||
this.error = ''
|
||||
this.authMethod = 'password'
|
||||
|
||||
try {
|
||||
await hybridAuthService.loginWithPassword(this.credentials.email, this.credentials.password)
|
||||
this.$router.push('/chat')
|
||||
} catch (error) {
|
||||
this.error = error.message || 'Password login failed'
|
||||
} finally {
|
||||
this.loading = false
|
||||
this.authMethod = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,69 +200,184 @@ export default {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.user-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
.auth-section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.user-button {
|
||||
.auth-section h3 {
|
||||
color: #374151;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.auth-divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.auth-divider::before,
|
||||
.auth-divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.auth-divider span {
|
||||
padding: 0 1rem;
|
||||
color: #9ca3af;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.azure-login-button {
|
||||
width: 100%;
|
||||
padding: 0.875rem 1rem;
|
||||
background: #0078d4;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.5rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 12px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
width: 100%;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.user-button:hover {
|
||||
border-color: #ffc407;
|
||||
box-shadow: 0 4px 12px rgba(255, 196, 7, 0.2);
|
||||
.azure-login-button:hover:not(:disabled) {
|
||||
background: #106ebe;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(16, 110, 190, 0.3);
|
||||
}
|
||||
|
||||
.admin-button:hover {
|
||||
border-color: #f59e0b;
|
||||
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.2);
|
||||
.azure-login-button:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.user-icon {
|
||||
font-size: 1.6rem;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
.azure-icon {
|
||||
font-size: 1.5rem;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #f3f4f6;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
text-align: left;
|
||||
.azure-text {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
.azure-title {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
font-size: 0.88rem;
|
||||
margin-bottom: 0.25rem;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.user-email {
|
||||
.azure-subtitle {
|
||||
font-weight: 400;
|
||||
font-size: 0.8rem;
|
||||
opacity: 0.9;
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.disabled-message {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
background: #f9fafb;
|
||||
color: #6b7280;
|
||||
font-size: 0.72rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e5e7eb;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.admin-button .user-icon {
|
||||
background: #fef3c7;
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.user-button .user-icon {
|
||||
background: #e0e7ff;
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 2px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
transition: all 0.2s;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
outline: none;
|
||||
border-color: #ffc407;
|
||||
box-shadow: 0 0 0 3px rgba(255, 196, 7, 0.1);
|
||||
}
|
||||
|
||||
.form-input:disabled {
|
||||
background-color: #f3f4f6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
background: #ffc407;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.login-button:hover:not(:disabled) {
|
||||
background: #e6b006;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(255, 196, 7, 0.3);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
background: #9ca3af;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
padding: 0.75rem;
|
||||
border-radius: 8px;
|
||||
border-left: 4px solid #dc2626;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.login-help {
|
||||
text-align: center;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.demo-info {
|
||||
background: #f0f9ff;
|
||||
color: #0369a1;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #bae6fd;
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,9 +7,32 @@ const api = axios.create({
|
|||
timeout: 30000,
|
||||
})
|
||||
|
||||
// Add authentication interceptor
|
||||
api.interceptors.request.use(
|
||||
async (config) => {
|
||||
// Try to get token from either auth method
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
const azureToken = localStorage.getItem('azureAuthToken');
|
||||
|
||||
const token = authToken || azureToken;
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => Promise.reject(error)
|
||||
)
|
||||
|
||||
api.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
// Token expired or invalid, clear all tokens and redirect to login
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('azureAuthToken');
|
||||
localStorage.removeItem('currentUser');
|
||||
window.location.href = '/login';
|
||||
}
|
||||
console.error('API Error:', error.response?.data || error.message)
|
||||
return Promise.reject(error)
|
||||
}
|
||||
|
|
|
|||
155
admin/src/services/authService.js
Normal file
155
admin/src/services/authService.js
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || '/api';
|
||||
|
||||
class AuthService {
|
||||
constructor() {
|
||||
this.token = localStorage.getItem('authToken');
|
||||
}
|
||||
|
||||
async login(email, password) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({ email, password }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Login failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Store token and user data
|
||||
this.token = data.token;
|
||||
localStorage.setItem('authToken', data.token);
|
||||
localStorage.setItem('currentUser', JSON.stringify(data.user));
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async validateToken() {
|
||||
if (!this.token) {
|
||||
throw new Error('No token available');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/validate`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Token validation failed');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Update stored user data
|
||||
localStorage.setItem('currentUser', JSON.stringify(data.user));
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
// Clear invalid token
|
||||
this.logout();
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
if (this.token) {
|
||||
try {
|
||||
await fetch(`${API_BASE_URL}/auth/logout`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Logout API call failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear local storage
|
||||
this.token = null;
|
||||
localStorage.removeItem('authToken');
|
||||
localStorage.removeItem('currentUser');
|
||||
}
|
||||
|
||||
async changePassword(currentPassword, newPassword) {
|
||||
if (!this.token) {
|
||||
throw new Error('Not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/auth/change-password`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.token}`,
|
||||
},
|
||||
body: JSON.stringify({ currentPassword, newPassword }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Password change failed');
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return !!this.token;
|
||||
}
|
||||
|
||||
getToken() {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
const userStr = localStorage.getItem('currentUser');
|
||||
return userStr ? JSON.parse(userStr) : null;
|
||||
}
|
||||
|
||||
// Interceptor for API calls to automatically add auth header
|
||||
async apiCall(endpoint, options = {}) {
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, config);
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token expired or invalid
|
||||
this.logout();
|
||||
window.location.href = '/login';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(error.message || 'Request failed');
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export default new AuthService();
|
||||
181
admin/src/services/azureAuthService.js
Normal file
181
admin/src/services/azureAuthService.js
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { PublicClientApplication } from '@azure/msal-browser'
|
||||
|
||||
class AzureAuthService {
|
||||
constructor() {
|
||||
// Azure AD configuration from the existing setup
|
||||
this.msalConfig = {
|
||||
auth: {
|
||||
clientId: '9079054c-9620-4757-a256-23413042f1ef',
|
||||
authority: 'https://login.microsoftonline.com/e519c2e6-bc6d-4fdf-8d9c-923c2f002385',
|
||||
redirectUri: window.location.origin + '/auth/callback', // Dynamic redirect for development
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: 'sessionStorage',
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
}
|
||||
|
||||
this.loginRequest = {
|
||||
scopes: ['user.read', 'profile', 'email']
|
||||
}
|
||||
|
||||
this.msalInstance = new PublicClientApplication(this.msalConfig)
|
||||
this.account = null
|
||||
|
||||
// Initialize on load
|
||||
this.initialize()
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
try {
|
||||
await this.msalInstance.initialize()
|
||||
|
||||
// Handle redirect response if returning from Azure AD
|
||||
const response = await this.msalInstance.handleRedirectPromise()
|
||||
if (response) {
|
||||
this.account = response.account
|
||||
localStorage.setItem('azureAuthToken', response.accessToken)
|
||||
localStorage.setItem('currentUser', JSON.stringify({
|
||||
id: response.account.localAccountId,
|
||||
email: response.account.username,
|
||||
name: response.account.name,
|
||||
role: 'user', // Will be determined by backend
|
||||
authMethod: 'azure'
|
||||
}))
|
||||
} else {
|
||||
// Check if user is already signed in
|
||||
const accounts = this.msalInstance.getAllAccounts()
|
||||
if (accounts.length > 0) {
|
||||
this.account = accounts[0]
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('MSAL initialization error:', error)
|
||||
}
|
||||
}
|
||||
|
||||
async loginWithPopup() {
|
||||
try {
|
||||
const response = await this.msalInstance.loginPopup(this.loginRequest)
|
||||
this.account = response.account
|
||||
|
||||
// Store Azure token and user info
|
||||
localStorage.setItem('azureAuthToken', response.accessToken)
|
||||
localStorage.setItem('currentUser', JSON.stringify({
|
||||
id: response.account.localAccountId,
|
||||
email: response.account.username,
|
||||
name: response.account.name,
|
||||
role: 'user', // Will be determined by backend
|
||||
authMethod: 'azure'
|
||||
}))
|
||||
|
||||
return response
|
||||
} catch (error) {
|
||||
console.error('Azure login error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async loginWithRedirect() {
|
||||
try {
|
||||
await this.msalInstance.loginRedirect(this.loginRequest)
|
||||
} catch (error) {
|
||||
console.error('Azure login redirect error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async logout() {
|
||||
try {
|
||||
// Clear local storage
|
||||
localStorage.removeItem('azureAuthToken')
|
||||
localStorage.removeItem('currentUser')
|
||||
localStorage.removeItem('authToken')
|
||||
|
||||
// Azure logout
|
||||
const logoutRequest = {
|
||||
account: this.account
|
||||
}
|
||||
|
||||
await this.msalInstance.logoutPopup(logoutRequest)
|
||||
this.account = null
|
||||
} catch (error) {
|
||||
console.error('Azure logout error:', error)
|
||||
// Clear local storage even if Azure logout fails
|
||||
localStorage.removeItem('azureAuthToken')
|
||||
localStorage.removeItem('currentUser')
|
||||
localStorage.removeItem('authToken')
|
||||
}
|
||||
}
|
||||
|
||||
async getToken() {
|
||||
if (!this.account) {
|
||||
throw new Error('No account available')
|
||||
}
|
||||
|
||||
try {
|
||||
const silentRequest = {
|
||||
...this.loginRequest,
|
||||
account: this.account
|
||||
}
|
||||
|
||||
const response = await this.msalInstance.acquireTokenSilent(silentRequest)
|
||||
localStorage.setItem('azureAuthToken', response.accessToken)
|
||||
return response.accessToken
|
||||
} catch (error) {
|
||||
console.error('Silent token acquisition failed:', error)
|
||||
|
||||
// Fallback to interactive token acquisition
|
||||
try {
|
||||
const response = await this.msalInstance.acquireTokenPopup(this.loginRequest)
|
||||
localStorage.setItem('azureAuthToken', response.accessToken)
|
||||
return response.accessToken
|
||||
} catch (interactiveError) {
|
||||
console.error('Interactive token acquisition failed:', interactiveError)
|
||||
throw interactiveError
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isAuthenticated() {
|
||||
return !!this.account && !!localStorage.getItem('azureAuthToken')
|
||||
}
|
||||
|
||||
getCurrentUser() {
|
||||
const userStr = localStorage.getItem('currentUser')
|
||||
return userStr ? JSON.parse(userStr) : null
|
||||
}
|
||||
|
||||
getAccount() {
|
||||
return this.account
|
||||
}
|
||||
|
||||
async validateWithBackend() {
|
||||
try {
|
||||
const token = await this.getToken()
|
||||
const response = await fetch('/api/auth/azure-validate', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Backend validation failed')
|
||||
}
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
// Update user data with backend response (includes role, permissions, etc.)
|
||||
localStorage.setItem('currentUser', JSON.stringify(data.user))
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('Backend validation error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new AzureAuthService()
|
||||
190
admin/src/services/hybridAuthService.js
Normal file
190
admin/src/services/hybridAuthService.js
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
import azureAuthService from './azureAuthService'
|
||||
import authService from './authService'
|
||||
|
||||
class HybridAuthService {
|
||||
constructor() {
|
||||
this.azureService = azureAuthService
|
||||
this.passwordService = authService
|
||||
this.passwordAuthEnabled = true // Default enabled, controlled by backend
|
||||
}
|
||||
|
||||
async initialize() {
|
||||
// Initialize Azure AD service
|
||||
await this.azureService.initialize()
|
||||
|
||||
// Check backend for password authentication setting
|
||||
await this.checkPasswordAuthSetting()
|
||||
}
|
||||
|
||||
async checkPasswordAuthSetting() {
|
||||
try {
|
||||
const response = await fetch('/api/auth/settings')
|
||||
if (response.ok) {
|
||||
const settings = await response.json()
|
||||
this.passwordAuthEnabled = settings.passwordAuthEnabled !== false
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Could not fetch auth settings, keeping password auth enabled')
|
||||
this.passwordAuthEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
// Azure AD login methods
|
||||
async loginWithAzure(useRedirect = false) {
|
||||
if (useRedirect) {
|
||||
return await this.azureService.loginWithRedirect()
|
||||
} else {
|
||||
const response = await this.azureService.loginWithPopup()
|
||||
|
||||
// Validate with backend to get role and permissions
|
||||
await this.azureService.validateWithBackend()
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
// Password login method
|
||||
async loginWithPassword(email, password) {
|
||||
if (!this.passwordAuthEnabled) {
|
||||
throw new Error('Password authentication is disabled')
|
||||
}
|
||||
|
||||
return await this.passwordService.login(email, password)
|
||||
}
|
||||
|
||||
// Universal logout
|
||||
async logout() {
|
||||
const currentUser = this.getCurrentUser()
|
||||
|
||||
if (currentUser?.authMethod === 'azure') {
|
||||
await this.azureService.logout()
|
||||
} else {
|
||||
await this.passwordService.logout()
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication status
|
||||
isAuthenticated() {
|
||||
const currentUser = this.getCurrentUser()
|
||||
|
||||
if (currentUser?.authMethod === 'azure') {
|
||||
return this.azureService.isAuthenticated()
|
||||
} else {
|
||||
return this.passwordService.isAuthenticated()
|
||||
}
|
||||
}
|
||||
|
||||
// Get current user (works for both auth methods)
|
||||
getCurrentUser() {
|
||||
return this.azureService.getCurrentUser() || this.passwordService.getCurrentUser()
|
||||
}
|
||||
|
||||
// Get appropriate token for API calls
|
||||
async getApiToken() {
|
||||
const currentUser = this.getCurrentUser()
|
||||
|
||||
if (currentUser?.authMethod === 'azure') {
|
||||
return await this.azureService.getToken()
|
||||
} else {
|
||||
return this.passwordService.getToken()
|
||||
}
|
||||
}
|
||||
|
||||
// Validate current session
|
||||
async validateSession() {
|
||||
const currentUser = this.getCurrentUser()
|
||||
|
||||
if (!currentUser) {
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
if (currentUser.authMethod === 'azure') {
|
||||
await this.azureService.validateWithBackend()
|
||||
} else {
|
||||
await this.passwordService.validateToken()
|
||||
}
|
||||
return true
|
||||
} catch (error) {
|
||||
console.warn('Session validation failed:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Check if password authentication is enabled
|
||||
isPasswordAuthEnabled() {
|
||||
return this.passwordAuthEnabled
|
||||
}
|
||||
|
||||
// Admin method to toggle password authentication
|
||||
async togglePasswordAuth(enabled) {
|
||||
try {
|
||||
const token = await this.getApiToken()
|
||||
const response = await fetch('/api/auth/settings/password-auth', {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify({ enabled })
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
this.passwordAuthEnabled = enabled
|
||||
return true
|
||||
}
|
||||
return false
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle password auth:', error)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// API call helper that handles both auth methods
|
||||
async apiCall(endpoint, options = {}) {
|
||||
try {
|
||||
const token = await this.getApiToken()
|
||||
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(token && { 'Authorization': `Bearer ${token}` }),
|
||||
...options.headers,
|
||||
},
|
||||
...options,
|
||||
}
|
||||
|
||||
const response = await fetch(`/api${endpoint}`, config)
|
||||
|
||||
if (response.status === 401) {
|
||||
// Token expired or invalid, logout user
|
||||
await this.logout()
|
||||
window.location.href = '/login'
|
||||
return
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json()
|
||||
throw new Error(error.message || 'Request failed')
|
||||
}
|
||||
|
||||
return response.json()
|
||||
} catch (error) {
|
||||
console.error('API call error:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Password change (only for password auth users)
|
||||
async changePassword(currentPassword, newPassword) {
|
||||
const currentUser = this.getCurrentUser()
|
||||
|
||||
if (currentUser?.authMethod === 'azure') {
|
||||
throw new Error('Password change not available for Azure AD users')
|
||||
}
|
||||
|
||||
return await this.passwordService.changePassword(currentPassword, newPassword)
|
||||
}
|
||||
}
|
||||
|
||||
export default new HybridAuthService()
|
||||
|
|
@ -7,6 +7,7 @@ require('dotenv').config();
|
|||
const { testConnection } = require('./config/database');
|
||||
const { generalLimiter } = require('./middleware/rateLimiter');
|
||||
const errorHandler = require('./middleware/errorHandler');
|
||||
const authRouter = require('./routes/auth');
|
||||
const chatRouter = require('./routes/chat');
|
||||
const assistantsRouter = require('./routes/assistants');
|
||||
const usersRouter = require('./routes/users');
|
||||
|
|
@ -48,6 +49,7 @@ app.get('/health', (req, res) => {
|
|||
});
|
||||
|
||||
// API routes
|
||||
app.use('/api/auth', authRouter);
|
||||
app.use('/api/chat', chatRouter);
|
||||
app.use('/api/assistants', assistantsRouter);
|
||||
app.use('/api/users', usersRouter);
|
||||
|
|
@ -64,6 +66,7 @@ app.get('/api', (req, res) => {
|
|||
endpoints: {
|
||||
health: '/health',
|
||||
api: '/api',
|
||||
auth: '/api/auth',
|
||||
chat: '/api/chat/completions',
|
||||
assistants: '/api/assistants',
|
||||
conversations: '/api/chat/conversations/:id/messages'
|
||||
|
|
|
|||
68
server/middleware/auth.js
Normal file
68
server/middleware/auth.js
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
const jwt = require('jsonwebtoken');
|
||||
const User = require('../models/User');
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production';
|
||||
|
||||
const authenticateToken = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access token required' });
|
||||
}
|
||||
|
||||
const decoded = jwt.verify(token, JWT_SECRET);
|
||||
|
||||
// Get user from database to ensure they still exist and are active
|
||||
const user = await User.findByPk(decoded.id);
|
||||
|
||||
if (!user || !user.isActive) {
|
||||
return res.status(403).json({ message: 'User not found or inactive' });
|
||||
}
|
||||
|
||||
req.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.preferences?.role || 'user',
|
||||
allowedAgents: user.preferences?.allowedAgents || null
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
if (error.name === 'TokenExpiredError') {
|
||||
return res.status(403).json({ message: 'Token expired' });
|
||||
}
|
||||
if (error.name === 'JsonWebTokenError') {
|
||||
return res.status(403).json({ message: 'Invalid token' });
|
||||
}
|
||||
return res.status(500).json({ message: 'Token verification failed' });
|
||||
}
|
||||
};
|
||||
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.user || req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: 'Admin privileges required' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
const generateToken = (user) => {
|
||||
return jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
role: user.preferences?.role || 'user'
|
||||
},
|
||||
JWT_SECRET,
|
||||
{ expiresIn: '24h' }
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
requireAdmin,
|
||||
generateToken,
|
||||
JWT_SECRET
|
||||
};
|
||||
134
server/middleware/azureAuth.js
Normal file
134
server/middleware/azureAuth.js
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
const jwt = require('jsonwebtoken');
|
||||
const { jwtDecode } = require('jwt-decode');
|
||||
const User = require('../models/User');
|
||||
|
||||
// Azure AD configuration
|
||||
const AZURE_TENANT_ID = 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385';
|
||||
const AZURE_CLIENT_ID = '9079054c-9620-4757-a256-23413042f1ef';
|
||||
|
||||
const validateAzureToken = async (req, res, next) => {
|
||||
try {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access token required' });
|
||||
}
|
||||
|
||||
// Decode the Azure AD token (without verification for now - in production should verify signature)
|
||||
let decoded;
|
||||
try {
|
||||
decoded = jwtDecode(token);
|
||||
} catch (error) {
|
||||
return res.status(403).json({ message: 'Invalid token format' });
|
||||
}
|
||||
|
||||
// Validate Azure AD token claims
|
||||
if (decoded.aud !== AZURE_CLIENT_ID) {
|
||||
return res.status(403).json({ message: 'Invalid token audience' });
|
||||
}
|
||||
|
||||
if (decoded.tid !== AZURE_TENANT_ID) {
|
||||
return res.status(403).json({ message: 'Invalid tenant' });
|
||||
}
|
||||
|
||||
if (decoded.exp * 1000 < Date.now()) {
|
||||
return res.status(403).json({ message: 'Token expired' });
|
||||
}
|
||||
|
||||
// Check if user exists in database, create if not
|
||||
let user = await User.findOne({ where: { email: decoded.email || decoded.upn } });
|
||||
|
||||
if (!user) {
|
||||
// Auto-provision new Azure AD user
|
||||
user = await User.create({
|
||||
email: decoded.email || decoded.upn,
|
||||
name: decoded.name || decoded.given_name + ' ' + decoded.family_name,
|
||||
password: 'azure-ad-user', // Placeholder - not used for Azure users
|
||||
preferences: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
defaultAssistant: 'creator-bot-push-the-boundaries-of-technology',
|
||||
role: getAzureUserRole(decoded.email || decoded.upn),
|
||||
allowedAgents: getAzureUserRole(decoded.email || decoded.upn) === 'admin' ? null : [],
|
||||
authMethod: 'azure'
|
||||
},
|
||||
isActive: true
|
||||
});
|
||||
} else {
|
||||
// Update last login for existing user
|
||||
await user.update({
|
||||
lastLoginAt: new Date(),
|
||||
preferences: {
|
||||
...user.preferences,
|
||||
authMethod: 'azure'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
return res.status(403).json({ message: 'User account is disabled' });
|
||||
}
|
||||
|
||||
// Attach user info to request
|
||||
req.user = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.preferences?.role || 'user',
|
||||
allowedAgents: user.preferences?.allowedAgents || null,
|
||||
authMethod: 'azure',
|
||||
azureId: decoded.oid || decoded.sub
|
||||
};
|
||||
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Azure token validation error:', error);
|
||||
return res.status(500).json({ message: 'Token validation failed' });
|
||||
}
|
||||
};
|
||||
|
||||
// Determine user role based on email or Azure group membership
|
||||
const getAzureUserRole = (email) => {
|
||||
// Admin users - add more emails as needed
|
||||
const adminEmails = [
|
||||
'daveporter@oliver.agency',
|
||||
// Add other admin emails here
|
||||
];
|
||||
|
||||
return adminEmails.includes(email.toLowerCase()) ? 'admin' : 'user';
|
||||
};
|
||||
|
||||
// Hybrid authentication middleware that handles both Azure AD and JWT tokens
|
||||
const hybridAuthenticate = async (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1];
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ message: 'Access token required' });
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to decode as Azure AD token first
|
||||
const decoded = jwtDecode(token);
|
||||
|
||||
// Check if it's an Azure AD token
|
||||
if (decoded.aud === AZURE_CLIENT_ID && decoded.tid === AZURE_TENANT_ID) {
|
||||
return validateAzureToken(req, res, next);
|
||||
} else {
|
||||
// Fall back to regular JWT validation
|
||||
const { authenticateToken } = require('./auth');
|
||||
return authenticateToken(req, res, next);
|
||||
}
|
||||
} catch (error) {
|
||||
// If decode fails, try regular JWT validation
|
||||
const { authenticateToken } = require('./auth');
|
||||
return authenticateToken(req, res, next);
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
validateAzureToken,
|
||||
hybridAuthenticate,
|
||||
getAzureUserRole
|
||||
};
|
||||
79
server/migrations/add-authentication-fields.js
Normal file
79
server/migrations/add-authentication-fields.js
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
const { sequelize } = require('../config/database');
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
const migration = {
|
||||
async up() {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
// Add password column
|
||||
await queryInterface.addColumn('users', 'password', {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: true, // Initially null, we'll update existing users
|
||||
});
|
||||
|
||||
// Add lastLoginAt column
|
||||
await queryInterface.addColumn('users', 'lastLoginAt', {
|
||||
type: sequelize.Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
// Update existing users with default passwords and ensure they have proper role setup
|
||||
const users = await sequelize.query('SELECT id, email FROM users', {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
});
|
||||
|
||||
for (const user of users) {
|
||||
// Hash default password for existing users
|
||||
const defaultPassword = 'changeMe123!';
|
||||
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
|
||||
|
||||
// Special handling for daveporter@oliver.agency
|
||||
if (user.email === 'daveporter@oliver.agency') {
|
||||
await sequelize.query(
|
||||
`UPDATE users SET
|
||||
password = :password,
|
||||
preferences = COALESCE(preferences, '{}'::jsonb) || '{"role": "admin", "allowedAgents": null}'::jsonb
|
||||
WHERE id = :id`,
|
||||
{
|
||||
replacements: { password: hashedPassword, id: user.id },
|
||||
type: sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// Regular users get no agents by default
|
||||
await sequelize.query(
|
||||
`UPDATE users SET
|
||||
password = :password,
|
||||
preferences = COALESCE(preferences, '{}'::jsonb) || '{"role": "user", "allowedAgents": []}'::jsonb
|
||||
WHERE id = :id`,
|
||||
{
|
||||
replacements: { password: hashedPassword, id: user.id },
|
||||
type: sequelize.QueryTypes.UPDATE,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Make password column required after updating existing records
|
||||
await queryInterface.changeColumn('users', 'password', {
|
||||
type: sequelize.Sequelize.STRING,
|
||||
allowNull: false,
|
||||
});
|
||||
|
||||
console.log('✅ Authentication fields added successfully');
|
||||
console.log('ℹ️ Existing users have been set with default password: changeMe123!');
|
||||
console.log('ℹ️ daveporter@oliver.agency has been set as admin');
|
||||
console.log('ℹ️ Other users have been set as regular users with no agent access');
|
||||
},
|
||||
|
||||
async down() {
|
||||
const queryInterface = sequelize.getQueryInterface();
|
||||
|
||||
await queryInterface.removeColumn('users', 'password');
|
||||
await queryInterface.removeColumn('users', 'lastLoginAt');
|
||||
|
||||
console.log('✅ Authentication fields removed');
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = migration;
|
||||
|
|
@ -19,12 +19,22 @@ const User = sequelize.define('User', {
|
|||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
password: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: false,
|
||||
},
|
||||
lastLoginAt: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
preferences: {
|
||||
type: DataTypes.JSONB,
|
||||
defaultValue: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
defaultAssistant: 'creator-bot-push-the-boundaries-of-technology',
|
||||
role: 'user',
|
||||
allowedAgents: null,
|
||||
},
|
||||
},
|
||||
isActive: {
|
||||
|
|
|
|||
179
server/package-lock.json
generated
179
server/package-lock.json
generated
|
|
@ -9,6 +9,8 @@
|
|||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.7.3",
|
||||
"bcrypt": "^6.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
|
|
@ -16,6 +18,8 @@
|
|||
"express-rate-limit": "^8.0.1",
|
||||
"helmet": "^8.1.0",
|
||||
"joi": "^18.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^2.0.2",
|
||||
"node-cache": "^5.1.2",
|
||||
|
|
@ -47,6 +51,38 @@
|
|||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-common": {
|
||||
"version": "15.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-15.12.0.tgz",
|
||||
"integrity": "sha512-4ucXbjVw8KJ5QBgnGJUeA07c8iznwlk5ioHIhI4ASXcXgcf2yRFhWzYOyWg/cI49LC9ekpFJeQtO3zjDTbl6TQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node": {
|
||||
"version": "3.7.3",
|
||||
"resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-3.7.3.tgz",
|
||||
"integrity": "sha512-MoJxkKM/YpChfq4g2o36tElyzNUMG8mfD6u8NbuaPAsqfGpaw249khAcJYNoIOigUzRw45OjXCOrexE6ImdUxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@azure/msal-common": "15.12.0",
|
||||
"jsonwebtoken": "^9.0.0",
|
||||
"uuid": "^8.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@azure/msal-node/node_modules/uuid": {
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
|
||||
"integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
|
|
@ -1935,6 +1971,20 @@
|
|||
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bcrypt": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
|
||||
"integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^8.3.0",
|
||||
"node-gyp-build": "^4.8.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
|
|
@ -2034,6 +2084,12 @@
|
|||
"node-int64": "^0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-equal-constant-time": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
|
||||
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
|
|
@ -2601,6 +2657,15 @@
|
|||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ecdsa-sig-formatter": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
|
||||
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
|
|
@ -4193,6 +4258,58 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jsonwebtoken": {
|
||||
"version": "9.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz",
|
||||
"integrity": "sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jws": "^3.2.2",
|
||||
"lodash.includes": "^4.3.0",
|
||||
"lodash.isboolean": "^3.0.3",
|
||||
"lodash.isinteger": "^4.0.4",
|
||||
"lodash.isnumber": "^3.0.3",
|
||||
"lodash.isplainobject": "^4.0.6",
|
||||
"lodash.isstring": "^4.0.1",
|
||||
"lodash.once": "^4.0.0",
|
||||
"ms": "^2.1.1",
|
||||
"semver": "^7.5.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12",
|
||||
"npm": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/jwa": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz",
|
||||
"integrity": "sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-equal-constant-time": "^1.0.1",
|
||||
"ecdsa-sig-formatter": "1.0.11",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/leven": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz",
|
||||
|
|
@ -4229,6 +4346,48 @@
|
|||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isboolean": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
|
||||
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isinteger": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
|
||||
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isnumber": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
|
||||
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isplainobject": {
|
||||
"version": "4.0.6",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
|
||||
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.isstring": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
|
||||
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.once": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
|
||||
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
|
|
@ -4586,6 +4745,15 @@
|
|||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
|
||||
"integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || >= 21"
|
||||
}
|
||||
},
|
||||
"node_modules/node-cache": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/node-cache/-/node-cache-5.1.2.tgz",
|
||||
|
|
@ -4636,6 +4804,17 @@
|
|||
"url": "https://opencollective.com/node-fetch"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build": {
|
||||
"version": "4.8.4",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
|
||||
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"node-gyp-build": "bin.js",
|
||||
"node-gyp-build-optional": "optional.js",
|
||||
"node-gyp-build-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"license": "ISC",
|
||||
"type": "commonjs",
|
||||
"dependencies": {
|
||||
"@azure/msal-node": "^3.7.3",
|
||||
"bcrypt": "^6.0.0",
|
||||
"chart.js": "^4.5.0",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^17.2.2",
|
||||
|
|
@ -22,6 +24,8 @@
|
|||
"express-rate-limit": "^8.0.1",
|
||||
"helmet": "^8.1.0",
|
||||
"joi": "^18.0.1",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"morgan": "^1.10.1",
|
||||
"multer": "^2.0.2",
|
||||
"node-cache": "^5.1.2",
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
const express = require('express');
|
||||
const { Assistant, User } = require('../models');
|
||||
const { hybridAuthenticate } = require('../middleware/azureAuth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
router.get('/', async (req, res, next) => {
|
||||
router.get('/', hybridAuthenticate, async (req, res, next) => {
|
||||
try {
|
||||
const { category, isActive = 'true', admin = 'false', userId } = req.query;
|
||||
|
||||
|
|
|
|||
230
server/routes/auth.js
Normal file
230
server/routes/auth.js
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
const express = require('express');
|
||||
const bcrypt = require('bcrypt');
|
||||
const User = require('../models/User');
|
||||
const { authenticateToken, generateToken } = require('../middleware/auth');
|
||||
const { validateAzureToken, hybridAuthenticate } = require('../middleware/azureAuth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Global setting for password authentication (stored in memory for now)
|
||||
let passwordAuthEnabled = true;
|
||||
|
||||
// Login endpoint (password-based)
|
||||
router.post('/login', async (req, res) => {
|
||||
try {
|
||||
// Check if password authentication is enabled
|
||||
if (!passwordAuthEnabled) {
|
||||
return res.status(403).json({ message: 'Password authentication is disabled. Please use Azure AD login.' });
|
||||
}
|
||||
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({ message: 'Email and password are required' });
|
||||
}
|
||||
|
||||
// Find user by email
|
||||
const user = await User.findOne({ where: { email: email.toLowerCase() } });
|
||||
|
||||
if (!user) {
|
||||
return res.status(401).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
if (!user.isActive) {
|
||||
return res.status(401).json({ message: 'Account is disabled' });
|
||||
}
|
||||
|
||||
// Verify password
|
||||
const validPassword = await bcrypt.compare(password, user.password);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ message: 'Invalid credentials' });
|
||||
}
|
||||
|
||||
// Update last login time
|
||||
await user.update({ lastLoginAt: new Date() });
|
||||
|
||||
// Generate token
|
||||
const token = generateToken(user);
|
||||
|
||||
res.json({
|
||||
message: 'Login successful',
|
||||
user: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
name: user.name,
|
||||
role: user.preferences?.role || 'user',
|
||||
allowedAgents: user.preferences?.allowedAgents || null
|
||||
},
|
||||
token
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Login error:', error);
|
||||
res.status(500).json({ message: 'Server error during login' });
|
||||
}
|
||||
});
|
||||
|
||||
// Token validation endpoint
|
||||
router.get('/validate', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
// User data is already validated and attached by middleware
|
||||
res.json({
|
||||
user: req.user,
|
||||
valid: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Token validation error:', error);
|
||||
res.status(500).json({ message: 'Server error during validation' });
|
||||
}
|
||||
});
|
||||
|
||||
// Logout endpoint
|
||||
router.post('/logout', authenticateToken, (req, res) => {
|
||||
// In a real application with token blacklisting, you would add the token to a blacklist here
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
|
||||
// Change password endpoint
|
||||
router.put('/change-password', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
|
||||
if (!currentPassword || !newPassword) {
|
||||
return res.status(400).json({ message: 'Current and new passwords are required' });
|
||||
}
|
||||
|
||||
if (newPassword.length < 8) {
|
||||
return res.status(400).json({ message: 'New password must be at least 8 characters' });
|
||||
}
|
||||
|
||||
const user = await User.findByPk(req.user.id);
|
||||
|
||||
// Verify current password
|
||||
const validPassword = await bcrypt.compare(currentPassword, user.password);
|
||||
if (!validPassword) {
|
||||
return res.status(401).json({ message: 'Current password is incorrect' });
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
|
||||
|
||||
// Update password
|
||||
await user.update({ password: hashedNewPassword });
|
||||
|
||||
res.json({ message: 'Password changed successfully' });
|
||||
} catch (error) {
|
||||
console.error('Change password error:', error);
|
||||
res.status(500).json({ message: 'Server error during password change' });
|
||||
}
|
||||
});
|
||||
|
||||
// Register new user (admin only for manual user creation)
|
||||
router.post('/register', authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { email, name, password, role = 'user' } = req.body;
|
||||
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: 'Admin privileges required to create users' });
|
||||
}
|
||||
|
||||
if (!email || !name || !password) {
|
||||
return res.status(400).json({ message: 'Email, name, and password are required' });
|
||||
}
|
||||
|
||||
if (password.length < 8) {
|
||||
return res.status(400).json({ message: 'Password must be at least 8 characters' });
|
||||
}
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await User.findOne({ where: { email: email.toLowerCase() } });
|
||||
if (existingUser) {
|
||||
return res.status(409).json({ message: 'User with this email already exists' });
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
// Create new user with default settings
|
||||
const newUser = await User.create({
|
||||
email: email.toLowerCase(),
|
||||
name,
|
||||
password: hashedPassword,
|
||||
preferences: {
|
||||
theme: 'light',
|
||||
notifications: true,
|
||||
defaultAssistant: 'creator-bot-push-the-boundaries-of-technology',
|
||||
role: role,
|
||||
allowedAgents: role === 'admin' ? null : [] // Admin gets all agents, users get none by default
|
||||
},
|
||||
isActive: true
|
||||
});
|
||||
|
||||
res.status(201).json({
|
||||
message: 'User created successfully',
|
||||
user: {
|
||||
id: newUser.id,
|
||||
email: newUser.email,
|
||||
name: newUser.name,
|
||||
role: newUser.preferences?.role || 'user'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Register user error:', error);
|
||||
res.status(500).json({ message: 'Server error during user registration' });
|
||||
}
|
||||
});
|
||||
|
||||
// Azure AD token validation endpoint
|
||||
router.post('/azure-validate', validateAzureToken, async (req, res) => {
|
||||
try {
|
||||
// User data is already validated and attached by middleware
|
||||
res.json({
|
||||
user: req.user,
|
||||
valid: true
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Azure token validation error:', error);
|
||||
res.status(500).json({ message: 'Server error during Azure validation' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get authentication settings
|
||||
router.get('/settings', (req, res) => {
|
||||
res.json({
|
||||
passwordAuthEnabled,
|
||||
azureAuthEnabled: true,
|
||||
tenantId: 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385',
|
||||
clientId: '9079054c-9620-4757-a256-23413042f1ef'
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle password authentication (admin only)
|
||||
router.put('/settings/password-auth', hybridAuthenticate, async (req, res) => {
|
||||
try {
|
||||
if (req.user.role !== 'admin') {
|
||||
return res.status(403).json({ message: 'Admin privileges required' });
|
||||
}
|
||||
|
||||
const { enabled } = req.body;
|
||||
|
||||
if (typeof enabled !== 'boolean') {
|
||||
return res.status(400).json({ message: 'Enabled must be a boolean value' });
|
||||
}
|
||||
|
||||
passwordAuthEnabled = enabled;
|
||||
|
||||
res.json({
|
||||
message: `Password authentication ${enabled ? 'enabled' : 'disabled'} successfully`,
|
||||
passwordAuthEnabled
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Toggle password auth error:', error);
|
||||
res.status(500).json({ message: 'Server error during settings update' });
|
||||
}
|
||||
});
|
||||
|
||||
// Get current password auth status (public endpoint)
|
||||
router.get('/password-enabled', (req, res) => {
|
||||
res.json({ enabled: passwordAuthEnabled });
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
@ -5,6 +5,7 @@ const { Assistant, Conversation, Message, User } = require('../models');
|
|||
const openaiService = require('../utils/openai');
|
||||
const responsesService = require('../utils/responses');
|
||||
const { chatLimiter } = require('../middleware/rateLimiter');
|
||||
const { hybridAuthenticate } = require('../middleware/azureAuth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
|
|
@ -57,7 +58,7 @@ async function generateTitleIfNeeded(conversation, userMessage, assistantRespons
|
|||
}
|
||||
}
|
||||
|
||||
router.post('/completions', upload.array('files', 5), async (req, res, next) => {
|
||||
router.post('/completions', hybridAuthenticate, upload.array('files', 5), async (req, res, next) => {
|
||||
try {
|
||||
// Handle form data parsing when files are present
|
||||
let messages, assistantKey, conversationId, userId, stream;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
const express = require('express');
|
||||
const { User } = require('../models');
|
||||
const { hybridAuthenticate } = require('../middleware/azureAuth');
|
||||
const { requireAdmin } = require('../middleware/auth');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Get all users (admin only)
|
||||
router.get('/', async (req, res, next) => {
|
||||
router.get('/', hybridAuthenticate, requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const users = await User.findAll({
|
||||
order: [['createdAt', 'DESC']],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue