Add SSO authentication and update UI styling - Added index.php with SSO authentication from NANO-RESEARCH - Added AuthMiddleware, JWTValidator, and auth endpoints - Updated logo to 25% size and left-aligned - Changed fonts to Montserrat - Added .htaccess for directory index
This commit is contained in:
parent
779be35576
commit
d64f6b19df
14 changed files with 1756 additions and 1 deletions
BIN
.DS_Store
vendored
BIN
.DS_Store
vendored
Binary file not shown.
13
.env.example copy
Normal file
13
.env.example copy
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# MSAL Authentication Configuration
|
||||
# Set SSO_ENABLED=true to require Microsoft login
|
||||
# Set SSO_ENABLED=false for local development (uses mock user)
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Azure AD Configuration (required when SSO_ENABLED=true)
|
||||
# Get these values from your Azure AD App Registration
|
||||
SSO_TENANT_ID=your-azure-tenant-id-here
|
||||
SSO_CLIENT_ID=your-azure-application-client-id-here
|
||||
|
||||
# Example values (replace with your actual Azure AD credentials):
|
||||
# SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
# SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -70,3 +70,10 @@ build/
|
|||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# PHP Composer
|
||||
vendor/
|
||||
composer.lock
|
||||
|
||||
# PHP Configuration (contains sensitive data)
|
||||
config.php
|
||||
|
|
|
|||
1
.htaccess
Normal file
1
.htaccess
Normal file
|
|
@ -0,0 +1 @@
|
|||
DirectoryIndex index.php index.html
|
||||
302
AUTH_README.md
Normal file
302
AUTH_README.md
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
# MSAL Authentication Setup Guide
|
||||
|
||||
## Overview
|
||||
PencilAutomator now includes Microsoft Authentication Library (MSAL) / Azure AD Single Sign-On (SSO) authentication. The authentication can be **toggled on/off** via environment variable for seamless testing and deployment.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Local Development (No Authentication)
|
||||
```bash
|
||||
# 1. Ensure .env file exists with:
|
||||
SSO_ENABLED=false
|
||||
|
||||
# 2. Run the app normally in MAMP or your PHP server
|
||||
# All users get mock "Local Developer" credentials
|
||||
# No login required
|
||||
```
|
||||
|
||||
### Production (with SSO)
|
||||
```bash
|
||||
# 1. Update .env file:
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=your-azure-tenant-id
|
||||
SSO_CLIENT_ID=your-azure-application-id
|
||||
|
||||
# 2. Deploy to server
|
||||
# 3. Users must login with Microsoft account
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Installation Steps
|
||||
|
||||
### 1. Install Dependencies
|
||||
```bash
|
||||
cd /Users/daveporter/Desktop/CODING-2024/pencil_automator
|
||||
composer install
|
||||
```
|
||||
|
||||
This installs the Firebase JWT library required for token validation.
|
||||
|
||||
### 2. Configure Environment
|
||||
```bash
|
||||
# Copy example file
|
||||
cp .env.example .env
|
||||
|
||||
# Edit .env and set:
|
||||
SSO_ENABLED=false # Start with authentication disabled
|
||||
```
|
||||
|
||||
### 3. Azure AD Setup (When Enabling SSO)
|
||||
|
||||
#### Create Azure AD App Registration:
|
||||
1. Go to [Azure Portal](https://portal.azure.com)
|
||||
2. Navigate to: **Azure Active Directory** → **App registrations** → **New registration**
|
||||
3. Set name: "PencilAutomator"
|
||||
4. Set redirect URI: `https://your-server-url.com/path/to/pencil_automator/index.php`
|
||||
5. Click **Register**
|
||||
|
||||
#### Get Credentials:
|
||||
1. Copy **Application (client) ID** → This is your `SSO_CLIENT_ID`
|
||||
2. Copy **Directory (tenant) ID** → This is your `SSO_TENANT_ID`
|
||||
3. Go to **Authentication** → Enable **ID tokens** checkbox
|
||||
4. Go to **API permissions** → Add: `openid`, `profile`, `email`
|
||||
|
||||
#### Update .env:
|
||||
```bash
|
||||
SSO_ENABLED=true
|
||||
SSO_TENANT_ID=e519c2e6-bc6d-4fdf-8d9c-923c2f002385
|
||||
SSO_CLIENT_ID=9079054c-9620-4757-a256-23413042f1ef
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### New Files Created:
|
||||
```
|
||||
/pencil_automator/
|
||||
├── composer.json # PHP dependencies (Firebase JWT)
|
||||
├── .env # Environment config (gitignored)
|
||||
├── .env.example # Template for environment variables
|
||||
├── env_loader.php # Loads .env file
|
||||
├── JWTValidator.php # JWT token validation logic
|
||||
├── AuthMiddleware.php # Auth orchestrator + login UI
|
||||
├── auth.php # Auth API endpoint
|
||||
├── auth-test.php # Debugging page
|
||||
├── config.php # Configuration (gitignored)
|
||||
├── AUTH_README.md # This file
|
||||
└── vendor/ # Composer dependencies (gitignored)
|
||||
```
|
||||
|
||||
### Modified Files:
|
||||
```
|
||||
.gitignore # Added .env, vendor/, config.php
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## How It Works
|
||||
|
||||
### When SSO_ENABLED=false (Testing Mode)
|
||||
1. User visits app
|
||||
2. AuthMiddleware returns mock "Local Developer" user
|
||||
3. No login page shown
|
||||
4. All features work normally
|
||||
5. Perfect for local testing
|
||||
|
||||
### When SSO_ENABLED=true (Production Mode)
|
||||
1. User visits app
|
||||
2. AuthMiddleware checks for `auth_token` cookie
|
||||
3. If no token → Show MSAL login page
|
||||
4. User clicks "Sign In with Microsoft"
|
||||
5. MSAL popup opens for Azure AD login
|
||||
6. User authenticates
|
||||
7. Token sent to `auth.php` for validation
|
||||
8. JWT validated against Azure AD public keys
|
||||
9. Token stored in httpOnly cookie (24 hours)
|
||||
10. User redirected to app
|
||||
11. Logout button visible in header
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Test Authentication Status
|
||||
Visit: `http://your-server/auth-test.php`
|
||||
|
||||
Shows:
|
||||
- SSO configuration (enabled/disabled)
|
||||
- Tenant ID and Client ID
|
||||
- Current authentication status
|
||||
- User information
|
||||
- Cookie presence
|
||||
|
||||
### Test Locally (SSO Disabled)
|
||||
```bash
|
||||
# 1. Set SSO_ENABLED=false in .env
|
||||
# 2. Open app in MAMP or PHP server
|
||||
# 3. Should see "Welcome, Local Developer" (if SSO was previously enabled)
|
||||
# 4. App functions normally
|
||||
# 5. No login/logout buttons
|
||||
```
|
||||
|
||||
### Test on Server (SSO Enabled)
|
||||
```bash
|
||||
# NOTE: Cannot test locally - Azure AD requires exact redirect URI match
|
||||
|
||||
# 1. Deploy to production server
|
||||
# 2. Set SSO_ENABLED=true in .env on server
|
||||
# 3. Add Azure AD credentials to .env
|
||||
# 4. Visit app URL
|
||||
# 5. Should see login page
|
||||
# 6. Click "Sign In with Microsoft"
|
||||
# 7. Complete Microsoft login
|
||||
# 8. Should redirect to app
|
||||
# 9. Should see "Welcome, [Your Name]" and logout button
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Security Features
|
||||
|
||||
✅ **httpOnly Cookies** - Prevents XSS attacks (JavaScript can't access token)
|
||||
✅ **SameSite=Lax** - Prevents CSRF attacks
|
||||
✅ **Secure Flag** - Cookie only sent over HTTPS in production
|
||||
✅ **JWT Validation** - Cryptographic verification of tokens
|
||||
✅ **Expiration Check** - Validates `exp` claim
|
||||
✅ **Not-Before Check** - Validates `nbf` claim
|
||||
✅ **Audience Validation** - Ensures token is for our app
|
||||
✅ **Issuer Validation** - Ensures token from Azure AD
|
||||
✅ **JWKS Verification** - Uses Azure AD public keys
|
||||
✅ **24-Hour Expiration** - Tokens expire after 1 day
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Login Page Shows But Can't Login
|
||||
- Check Azure AD app registration has correct redirect URI
|
||||
- Ensure `SSO_TENANT_ID` and `SSO_CLIENT_ID` are correct
|
||||
- Check browser console for MSAL errors
|
||||
- Visit `auth-test.php` to verify configuration
|
||||
|
||||
### "Authentication Required" Error
|
||||
- Check `auth_token` cookie exists (browser dev tools)
|
||||
- Token may have expired (24-hour limit)
|
||||
- Try logging out and back in
|
||||
- Check `auth-test.php` for token status
|
||||
|
||||
### SSO Not Disabling
|
||||
- Verify `.env` has `SSO_ENABLED=false` (not "false" in quotes)
|
||||
- Clear browser cookies
|
||||
- Restart PHP server/MAMP
|
||||
- Check `auth-test.php` shows "SSO Enabled: NO"
|
||||
|
||||
### Token Validation Failing
|
||||
- Check server can reach Azure AD endpoints
|
||||
- Verify tenant ID and client ID match Azure AD
|
||||
- Check token hasn't expired
|
||||
- Review `error_log` for JWT validation details
|
||||
|
||||
---
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Login
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "login",
|
||||
"idToken": "eyJ0eXAiOiJKV1QiLCJhbGci...",
|
||||
"accessToken": "eyJ0eXAiOiJKV1QiLCJhbGci..."
|
||||
}
|
||||
```
|
||||
|
||||
### Logout
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "logout"
|
||||
}
|
||||
```
|
||||
|
||||
### Status Check
|
||||
```http
|
||||
POST /auth.php
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"action": "status"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Rotating Credentials
|
||||
1. Update Azure AD app registration
|
||||
2. Update `.env` with new credentials
|
||||
3. No code changes needed
|
||||
4. Existing sessions remain valid until cookie expires
|
||||
|
||||
### Disabling SSO Temporarily
|
||||
```bash
|
||||
# In .env:
|
||||
SSO_ENABLED=false
|
||||
|
||||
# Immediately disables SSO for all users
|
||||
# No restart needed
|
||||
# Users get mock "Local Developer" access
|
||||
```
|
||||
|
||||
### Monitoring
|
||||
- Check `error_log` for authentication failures
|
||||
- Monitor Azure AD sign-in logs
|
||||
- Track failed login attempts
|
||||
- Review token validation errors
|
||||
|
||||
---
|
||||
|
||||
## Production Checklist
|
||||
|
||||
Before enabling SSO in production:
|
||||
|
||||
- [ ] Composer dependencies installed (`vendor/` directory exists)
|
||||
- [ ] `.env` file configured with Azure AD credentials
|
||||
- [ ] Azure AD app registration created
|
||||
- [ ] Redirect URI matches production URL exactly
|
||||
- [ ] ID tokens enabled in Azure AD app
|
||||
- [ ] API permissions added (`openid`, `profile`, `email`)
|
||||
- [ ] HTTPS enabled on production server
|
||||
- [ ] `auth-test.php` shows correct configuration
|
||||
- [ ] Test login/logout flow works
|
||||
- [ ] Error logging enabled
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For issues with:
|
||||
- **MSAL errors**: Check [MSAL.js documentation](https://github.com/AzureAD/microsoft-authentication-library-for-js)
|
||||
- **Azure AD setup**: Check [Azure AD app registration guide](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app)
|
||||
- **JWT validation**: Check Firebase JWT library logs in `error_log`
|
||||
- **Configuration**: Run `auth-test.php` to see current setup
|
||||
|
||||
---
|
||||
|
||||
## Important Notes
|
||||
|
||||
- **Cannot test MSAL locally** - Azure AD requires exact URL match
|
||||
- **Testing happens on server** after deployment
|
||||
- **SSO toggle allows testing without auth** before enabling
|
||||
- **httpOnly cookies** mean token not accessible via JavaScript
|
||||
- **24-hour token expiration** - users must re-login daily
|
||||
- **Mock user** (`dev@localhost`) used when SSO disabled
|
||||
409
AuthMiddleware.php
Normal file
409
AuthMiddleware.php
Normal file
|
|
@ -0,0 +1,409 @@
|
|||
<?php
|
||||
/**
|
||||
* Authentication Middleware for MSAL / Azure AD
|
||||
* Central authentication orchestrator with login UI
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/config.php';
|
||||
|
||||
class AuthMiddleware {
|
||||
private $validator;
|
||||
private $tenantId;
|
||||
private $clientId;
|
||||
private $ssoEnabled;
|
||||
|
||||
public function __construct() {
|
||||
$this->ssoEnabled = SSO_ENABLED;
|
||||
$this->tenantId = SSO_TENANT_ID;
|
||||
$this->clientId = SSO_CLIENT_ID;
|
||||
|
||||
// Only initialize validator if SSO is enabled
|
||||
if ($this->ssoEnabled) {
|
||||
require_once __DIR__ . '/JWTValidator.php';
|
||||
$this->validator = new JWTValidator($this->tenantId, $this->clientId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if SSO is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isSSOEnabled() {
|
||||
return $this->ssoEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is authenticated
|
||||
*
|
||||
* @return array ['authenticated' => bool, 'user' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function isAuthenticated() {
|
||||
// If SSO is disabled, return authenticated with mock user
|
||||
if (!$this->ssoEnabled) {
|
||||
return [
|
||||
'authenticated' => true,
|
||||
'user' => [
|
||||
'name' => 'Local Developer',
|
||||
'preferred_username' => 'dev@localhost'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
// Get token from cookie
|
||||
$token = $this->getTokenFromCookie();
|
||||
if (!$token) {
|
||||
return ['authenticated' => false, 'error' => 'No authentication token found'];
|
||||
}
|
||||
|
||||
// Validate token
|
||||
$validation = $this->validator->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
return ['authenticated' => false, 'error' => $validation['error']];
|
||||
}
|
||||
|
||||
return ['authenticated' => true, 'user' => $validation['payload']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Require authentication - blocks if not authenticated
|
||||
*
|
||||
* @return array User data
|
||||
*/
|
||||
public function requireAuth() {
|
||||
// If SSO is disabled, return mock user
|
||||
if (!$this->ssoEnabled) {
|
||||
return [
|
||||
'name' => 'Local Developer',
|
||||
'preferred_username' => 'dev@localhost'
|
||||
];
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
$auth = $this->isAuthenticated();
|
||||
|
||||
if (!$auth['authenticated']) {
|
||||
$this->handleUnauthorized($auth['error'] ?? 'Authentication required');
|
||||
exit;
|
||||
}
|
||||
|
||||
return $auth['user'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set authentication token (after login)
|
||||
*
|
||||
* @param string $token JWT token from MSAL
|
||||
* @return array ['success' => bool, 'user' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function setAuthToken($token) {
|
||||
if (!$this->ssoEnabled) {
|
||||
return ['success' => false, 'error' => 'SSO is disabled'];
|
||||
}
|
||||
|
||||
// Validate token
|
||||
$validation = $this->validator->validateToken($token);
|
||||
if (!$validation['valid']) {
|
||||
return ['success' => false, 'error' => $validation['error']];
|
||||
}
|
||||
|
||||
// Set httpOnly cookie with security options
|
||||
$cookieOptions = [
|
||||
'expires' => time() + (24 * 60 * 60), // 24 hours
|
||||
'path' => '/',
|
||||
'domain' => '',
|
||||
'secure' => isset($_SERVER['HTTPS']), // Only over HTTPS in production
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax'
|
||||
];
|
||||
|
||||
setcookie('auth_token', $token, $cookieOptions);
|
||||
|
||||
return ['success' => true, 'user' => $validation['payload']];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear authentication token (logout)
|
||||
*/
|
||||
public function clearAuthToken() {
|
||||
setcookie('auth_token', '', [
|
||||
'expires' => time() - 3600,
|
||||
'path' => '/',
|
||||
'httponly' => true
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get token from cookie
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
private function getTokenFromCookie() {
|
||||
return isset($_COOKIE['auth_token']) ? $_COOKIE['auth_token'] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle unauthorized access
|
||||
*
|
||||
* @param string $error Error message
|
||||
*/
|
||||
private function handleUnauthorized($error) {
|
||||
if ($this->isAjaxRequest()) {
|
||||
// For AJAX requests, return JSON
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'error' => 'Authentication required',
|
||||
'message' => $error,
|
||||
'requiresAuth' => true
|
||||
]);
|
||||
} else {
|
||||
// For page requests, show login interface
|
||||
$this->showLoginPage($error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if request is AJAX
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
private function isAjaxRequest() {
|
||||
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) &&
|
||||
strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest';
|
||||
}
|
||||
|
||||
/**
|
||||
* Show login page with MSAL integration
|
||||
*
|
||||
* @param string $error Optional error message
|
||||
*/
|
||||
public function showLoginPage($error = '') {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sign In - PencilAutomator</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<script src="https://alcdn.msauth.net/browser/2.15.0/js/msal-browser.min.js" crossorigin="anonymous"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
background: #1e1e1e;
|
||||
color: #fff;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.login-container {
|
||||
background: #2c2c2c;
|
||||
border: 2px solid #18A0FB;
|
||||
border-radius: 12px;
|
||||
padding: 50px 40px;
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
box-shadow: 0 10px 50px rgba(24, 160, 251, 0.2);
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
font-size: 4rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: #18A0FB;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
font-size: 1rem;
|
||||
color: #999;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
background: #ff4444;
|
||||
color: #fff;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
background: linear-gradient(135deg, #18A0FB 0%, #0d99ff 100%);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 18px 40px;
|
||||
border-radius: 8px;
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 1.1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 5px 20px rgba(24, 160, 251, 0.4);
|
||||
}
|
||||
|
||||
.btn-login:active {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.loading {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading.show {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 3px solid #fff;
|
||||
border-top: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.info-text {
|
||||
margin-top: 30px;
|
||||
font-size: 0.85rem;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-logo">✏️</div>
|
||||
<h1 class="login-title">PencilAutomator</h1>
|
||||
<p class="login-subtitle">Figma Plugin</p>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="error-message">
|
||||
<?php echo htmlspecialchars($error); ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<button class="btn-login" onclick="signIn()" id="loginBtn">
|
||||
<span id="lockIcon">🔒</span>
|
||||
<span id="loadingIcon" class="loading"></span>
|
||||
<span id="btnText">Sign In with Microsoft</span>
|
||||
</button>
|
||||
|
||||
<div class="info-text">
|
||||
Sign in with your Microsoft account to access the plugin
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const msalConfig = {
|
||||
auth: {
|
||||
clientId: "<?php echo $this->clientId; ?>",
|
||||
authority: "https://login.microsoftonline.com/<?php echo $this->tenantId; ?>",
|
||||
redirectUri: window.location.origin + window.location.pathname
|
||||
},
|
||||
cache: {
|
||||
cacheLocation: "sessionStorage",
|
||||
storeAuthStateInCookie: true,
|
||||
}
|
||||
};
|
||||
|
||||
const loginRequest = {
|
||||
scopes: ["openid", "profile", "email"],
|
||||
prompt: "select_account"
|
||||
};
|
||||
|
||||
const myMSALObj = new msal.PublicClientApplication(msalConfig);
|
||||
|
||||
function signIn() {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const lockIcon = document.getElementById('lockIcon');
|
||||
const loadingIcon = document.getElementById('loadingIcon');
|
||||
const btnText = document.getElementById('btnText');
|
||||
|
||||
// Show loading state
|
||||
loginBtn.disabled = true;
|
||||
lockIcon.style.display = 'none';
|
||||
loadingIcon.classList.add('show');
|
||||
btnText.textContent = 'Signing in...';
|
||||
|
||||
myMSALObj.loginPopup(loginRequest)
|
||||
.then(loginResponse => {
|
||||
// Send token to server for validation and cookie setting
|
||||
fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
action: 'login',
|
||||
idToken: loginResponse.idToken,
|
||||
accessToken: loginResponse.accessToken
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.reload();
|
||||
} else {
|
||||
alert('Authentication failed: ' + (data.error || 'Unknown error'));
|
||||
resetButton();
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Authentication error:', error);
|
||||
alert('Authentication failed. Please try again.');
|
||||
resetButton();
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
console.error("Login failed:", error);
|
||||
alert('Login failed: ' + error.message);
|
||||
resetButton();
|
||||
});
|
||||
}
|
||||
|
||||
function resetButton() {
|
||||
const loginBtn = document.getElementById('loginBtn');
|
||||
const lockIcon = document.getElementById('lockIcon');
|
||||
const loadingIcon = document.getElementById('loadingIcon');
|
||||
const btnText = document.getElementById('btnText');
|
||||
|
||||
loginBtn.disabled = false;
|
||||
lockIcon.style.display = 'inline';
|
||||
loadingIcon.classList.remove('show');
|
||||
btnText.textContent = 'Sign In with Microsoft';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
201
JWTValidator.php
Normal file
201
JWTValidator.php
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
/**
|
||||
* JWT Token Validator for Azure AD / MSAL
|
||||
* Validates JWT tokens from Microsoft Azure AD using JWKS
|
||||
*/
|
||||
|
||||
// Load Composer autoload for Firebase JWT library
|
||||
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||
require_once __DIR__ . '/vendor/autoload.php';
|
||||
}
|
||||
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\JWK;
|
||||
|
||||
class JWTValidator {
|
||||
private $tenantId;
|
||||
private $clientId;
|
||||
private $jwksCache = null;
|
||||
private $jwksCacheTime = 0;
|
||||
private $jwksCacheDuration = 3600; // Cache for 1 hour
|
||||
|
||||
public function __construct($tenantId, $clientId) {
|
||||
$this->tenantId = $tenantId;
|
||||
$this->clientId = $clientId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a JWT token
|
||||
*
|
||||
* @param string $token The JWT token to validate
|
||||
* @return array ['valid' => bool, 'payload' => array|null, 'error' => string|null]
|
||||
*/
|
||||
public function validateToken($token) {
|
||||
if (empty($token)) {
|
||||
return ['valid' => false, 'error' => 'Token is empty'];
|
||||
}
|
||||
|
||||
try {
|
||||
// Get public keys from Azure AD
|
||||
$jwks = $this->getJWKS();
|
||||
if (!$jwks) {
|
||||
return ['valid' => false, 'error' => 'Could not retrieve public keys from Azure AD'];
|
||||
}
|
||||
|
||||
// Convert JWKS to Key objects
|
||||
$keys = JWK::parseKeySet($jwks);
|
||||
|
||||
// Decode and validate the JWT
|
||||
$decoded = JWT::decode($token, $keys);
|
||||
$payload = (array) $decoded;
|
||||
|
||||
// Validate claims
|
||||
$validation = $this->validateClaims($payload);
|
||||
if (!$validation['valid']) {
|
||||
return $validation;
|
||||
}
|
||||
|
||||
return ['valid' => true, 'payload' => $payload];
|
||||
|
||||
} catch (\Firebase\JWT\ExpiredException $e) {
|
||||
return ['valid' => false, 'error' => 'Token has expired'];
|
||||
} catch (\Firebase\JWT\SignatureInvalidException $e) {
|
||||
return ['valid' => false, 'error' => 'Token signature is invalid'];
|
||||
} catch (\Firebase\JWT\BeforeValidException $e) {
|
||||
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
||||
} catch (Exception $e) {
|
||||
return ['valid' => false, 'error' => 'JWT validation failed: ' . $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate JWT claims
|
||||
*
|
||||
* @param array $payload The decoded JWT payload
|
||||
* @return array ['valid' => bool, 'error' => string|null]
|
||||
*/
|
||||
private function validateClaims($payload) {
|
||||
$now = time();
|
||||
|
||||
// Check expiration (exp claim)
|
||||
if (isset($payload['exp']) && $payload['exp'] < $now) {
|
||||
return ['valid' => false, 'error' => 'Token has expired'];
|
||||
}
|
||||
|
||||
// Check not-before (nbf claim)
|
||||
if (isset($payload['nbf']) && $payload['nbf'] > $now) {
|
||||
return ['valid' => false, 'error' => 'Token is not yet valid'];
|
||||
}
|
||||
|
||||
// Validate audience (aud claim) - must be our client ID or Microsoft Graph
|
||||
if (isset($payload['aud'])) {
|
||||
$validAudiences = [
|
||||
$this->clientId,
|
||||
'00000003-0000-0000-c000-000000000000', // Microsoft Graph
|
||||
'https://graph.microsoft.com'
|
||||
];
|
||||
|
||||
if (!in_array($payload['aud'], $validAudiences)) {
|
||||
return ['valid' => false, 'error' => 'Invalid audience: ' . $payload['aud']];
|
||||
}
|
||||
}
|
||||
|
||||
// Validate issuer (iss claim) - must be from Azure AD tenant
|
||||
if (isset($payload['iss'])) {
|
||||
$validIssuers = [
|
||||
"https://login.microsoftonline.com/{$this->tenantId}/v2.0",
|
||||
"https://login.microsoftonline.com/{$this->tenantId}/",
|
||||
"https://sts.windows.net/{$this->tenantId}/",
|
||||
"https://login.microsoftonline.com/common/v2.0"
|
||||
];
|
||||
|
||||
if (!in_array($payload['iss'], $validIssuers)) {
|
||||
return ['valid' => false, 'error' => 'Invalid issuer: ' . $payload['iss']];
|
||||
}
|
||||
}
|
||||
|
||||
return ['valid' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get JWKS (JSON Web Key Set) from Azure AD
|
||||
*
|
||||
* @return array|null
|
||||
*/
|
||||
private function getJWKS() {
|
||||
// Return cached JWKS if still valid
|
||||
if ($this->jwksCache && (time() - $this->jwksCacheTime) < $this->jwksCacheDuration) {
|
||||
return $this->jwksCache;
|
||||
}
|
||||
|
||||
// Get OpenID configuration from Azure AD
|
||||
$configUrl = "https://login.microsoftonline.com/{$this->tenantId}/v2.0/.well-known/openid-configuration";
|
||||
|
||||
$config = $this->fetchJson($configUrl);
|
||||
if (!$config || !isset($config['jwks_uri'])) {
|
||||
error_log("Failed to get OpenID configuration from: $configUrl");
|
||||
return null;
|
||||
}
|
||||
|
||||
// Fetch JWKS from jwks_uri
|
||||
$jwks = $this->fetchJson($config['jwks_uri']);
|
||||
|
||||
if (!$jwks || !isset($jwks['keys'])) {
|
||||
error_log("Failed to get JWKS from: " . $config['jwks_uri']);
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure all keys have the 'alg' parameter
|
||||
foreach ($jwks['keys'] as &$key) {
|
||||
if (!isset($key['alg'])) {
|
||||
// Default to RS256 for RSA keys
|
||||
if (isset($key['kty']) && $key['kty'] === 'RSA') {
|
||||
$key['alg'] = 'RS256';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache the JWKS
|
||||
$this->jwksCache = $jwks;
|
||||
$this->jwksCacheTime = time();
|
||||
|
||||
return $jwks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch JSON from a URL
|
||||
*
|
||||
* @param string $url
|
||||
* @return array|null
|
||||
*/
|
||||
private function fetchJson($url) {
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
error_log("CURL Error fetching $url: $curlError");
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
error_log("HTTP Error $httpCode fetching $url");
|
||||
return null;
|
||||
}
|
||||
|
||||
$json = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
error_log("JSON decode error fetching $url: " . json_last_error_msg());
|
||||
return null;
|
||||
}
|
||||
|
||||
return $json;
|
||||
}
|
||||
}
|
||||
123
auth-test.php
Normal file
123
auth-test.php
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
<?php
|
||||
/**
|
||||
* Authentication Test Page
|
||||
* Shows current authentication status and configuration
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/AuthMiddleware.php';
|
||||
|
||||
$auth = new AuthMiddleware();
|
||||
$authStatus = $auth->isAuthenticated();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Auth Test - PencilAutomator</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: #1e1e1e;
|
||||
color: #0f0;
|
||||
padding: 40px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
color: #18A0FB;
|
||||
border-bottom: 2px solid #18A0FB;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.section {
|
||||
background: #2c2c2c;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
.label {
|
||||
color: #ff0;
|
||||
font-weight: bold;
|
||||
}
|
||||
.value {
|
||||
color: #0ff;
|
||||
}
|
||||
.success {
|
||||
color: #0f0;
|
||||
}
|
||||
.error {
|
||||
color: #f00;
|
||||
}
|
||||
pre {
|
||||
background: #000;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>🔐 Authentication Test Page</h1>
|
||||
|
||||
<div class="section">
|
||||
<h2>SSO Configuration</h2>
|
||||
<p><span class="label">SSO Enabled:</span> <span class="value"><?php echo SSO_ENABLED ? 'YES' : 'NO'; ?></span></p>
|
||||
<p><span class="label">Tenant ID:</span> <span class="value"><?php echo SSO_TENANT_ID ?: '(not set)'; ?></span></p>
|
||||
<p><span class="label">Client ID:</span> <span class="value"><?php echo SSO_CLIENT_ID ?: '(not set)'; ?></span></p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Authentication Status</h2>
|
||||
<p><span class="label">Authenticated:</span>
|
||||
<span class="<?php echo $authStatus['authenticated'] ? 'success' : 'error'; ?>">
|
||||
<?php echo $authStatus['authenticated'] ? 'YES ✓' : 'NO ✗'; ?>
|
||||
</span>
|
||||
</p>
|
||||
|
||||
<?php if ($authStatus['authenticated']): ?>
|
||||
<p><span class="label">User Name:</span> <span class="value"><?php echo htmlspecialchars($authStatus['user']['name'] ?? 'Unknown'); ?></span></p>
|
||||
<p><span class="label">Email:</span> <span class="value"><?php echo htmlspecialchars($authStatus['user']['preferred_username'] ?? $authStatus['user']['upn'] ?? 'Unknown'); ?></span></p>
|
||||
<?php else: ?>
|
||||
<p><span class="label">Error:</span> <span class="error"><?php echo htmlspecialchars($authStatus['error'] ?? 'Not authenticated'); ?></span></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>Cookie Information</h2>
|
||||
<p><span class="label">Auth Token Cookie:</span>
|
||||
<span class="<?php echo isset($_COOKIE['auth_token']) ? 'success' : 'error'; ?>">
|
||||
<?php echo isset($_COOKIE['auth_token']) ? 'Present ✓' : 'Not Found ✗'; ?>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ($authStatus['authenticated'] && isset($authStatus['user'])): ?>
|
||||
<div class="section">
|
||||
<h2>User Payload (Debug)</h2>
|
||||
<pre><?php echo htmlspecialchars(json_encode($authStatus['user'], JSON_PRETTY_PRINT)); ?></pre>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="section">
|
||||
<h2>Actions</h2>
|
||||
<p><a href="index.php" style="color: #18A0FB;">← Back to Application</a></p>
|
||||
<?php if ($authStatus['authenticated'] && SSO_ENABLED): ?>
|
||||
<p><a href="#" onclick="logout()" style="color: #f00;">Logout</a></p>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function logout() {
|
||||
fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'logout' })
|
||||
})
|
||||
.then(() => {
|
||||
window.location.reload();
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
118
auth.php
Normal file
118
auth.php
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
<?php
|
||||
/**
|
||||
* Authentication API Endpoint
|
||||
* Handles login, logout, and status requests
|
||||
*/
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
require_once __DIR__ . '/AuthMiddleware.php';
|
||||
|
||||
$auth = new AuthMiddleware();
|
||||
|
||||
// Get POST data
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
|
||||
if (!$input || !isset($input['action'])) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Invalid request - action required']);
|
||||
exit;
|
||||
}
|
||||
|
||||
$action = $input['action'];
|
||||
|
||||
// Handle different actions
|
||||
switch ($action) {
|
||||
case 'login':
|
||||
handleLogin($auth, $input);
|
||||
break;
|
||||
|
||||
case 'logout':
|
||||
handleLogout($auth);
|
||||
break;
|
||||
|
||||
case 'status':
|
||||
handleStatus($auth);
|
||||
break;
|
||||
|
||||
default:
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Unknown action: ' . $action]);
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle login action
|
||||
*/
|
||||
function handleLogin($auth, $input) {
|
||||
if (!$auth->isSSOEnabled()) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'SSO is disabled']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer ID token for validation, fallback to access token
|
||||
$token = $input['idToken'] ?? $input['accessToken'] ?? null;
|
||||
|
||||
if (!$token) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['error' => 'Authentication token is required']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate and set token
|
||||
$result = $auth->setAuthToken($token);
|
||||
|
||||
if ($result['success']) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Authentication successful',
|
||||
'user' => [
|
||||
'name' => $result['user']['name'] ?? 'Unknown',
|
||||
'email' => $result['user']['preferred_username'] ?? $result['user']['upn'] ?? 'Unknown'
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $result['error']
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle logout action
|
||||
*/
|
||||
function handleLogout($auth) {
|
||||
$auth->clearAuthToken();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Logged out successfully'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle status check action
|
||||
*/
|
||||
function handleStatus($auth) {
|
||||
$authStatus = $auth->isAuthenticated();
|
||||
|
||||
if ($authStatus['authenticated']) {
|
||||
echo json_encode([
|
||||
'authenticated' => true,
|
||||
'sso_enabled' => $auth->isSSOEnabled(),
|
||||
'user' => [
|
||||
'name' => $authStatus['user']['name'] ?? 'Unknown',
|
||||
'email' => $authStatus['user']['preferred_username'] ?? $authStatus['user']['upn'] ?? 'Unknown'
|
||||
]
|
||||
]);
|
||||
} else {
|
||||
http_response_code(401);
|
||||
echo json_encode([
|
||||
'authenticated' => false,
|
||||
'sso_enabled' => $auth->isSSOEnabled(),
|
||||
'error' => $authStatus['error'] ?? 'Not authenticated'
|
||||
]);
|
||||
}
|
||||
}
|
||||
20
composer.json
Normal file
20
composer.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "pencil-automator/figma-plugin",
|
||||
"description": "Figma Plugin with MSAL Authentication",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=7.4",
|
||||
"firebase/php-jwt": "^6.0"
|
||||
},
|
||||
"require-dev": {},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"JWTValidator.php",
|
||||
"AuthMiddleware.php"
|
||||
]
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
"sort-packages": true
|
||||
}
|
||||
}
|
||||
55
env_loader.php
Normal file
55
env_loader.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
/**
|
||||
* Environment Variable Loader
|
||||
* Parses .env file and sets environment variables
|
||||
*/
|
||||
|
||||
function loadEnvFile($path = null) {
|
||||
// Default to .env file in same directory
|
||||
$path = $path ?? __DIR__ . '/.env';
|
||||
|
||||
// Check if .env file exists
|
||||
if (!file_exists($path)) {
|
||||
error_log("Warning: .env file not found at: $path");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Read file line by line
|
||||
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
|
||||
if ($lines === false) {
|
||||
error_log("Error: Could not read .env file at: $path");
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
// Skip comments
|
||||
if (strpos(trim($line), '#') === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parse KEY=VALUE format
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
|
||||
// Trim whitespace
|
||||
$key = trim($key);
|
||||
$value = trim($value);
|
||||
|
||||
// Remove surrounding quotes if present
|
||||
if (preg_match('/^(["\'])(.*)\\1$/', $value, $matches)) {
|
||||
$value = $matches[2];
|
||||
}
|
||||
|
||||
// Set environment variable
|
||||
putenv("$key=$value");
|
||||
$_ENV[$key] = $value;
|
||||
$_SERVER[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Auto-load .env on include
|
||||
loadEnvFile();
|
||||
119
index.php
Normal file
119
index.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
/**
|
||||
* Pencil Automator - Main Entry Point
|
||||
* Requires authentication via SSO (or mock user if SSO disabled)
|
||||
*/
|
||||
|
||||
// Require authentication
|
||||
$user = ['name' => 'User', 'preferred_username' => 'user@localhost'];
|
||||
$ssoEnabled = false;
|
||||
|
||||
try {
|
||||
if (file_exists(__DIR__ . '/AuthMiddleware.php')) {
|
||||
require_once 'AuthMiddleware.php';
|
||||
$auth = new AuthMiddleware();
|
||||
$user = $auth->requireAuth(); // Blocks if not authenticated
|
||||
$ssoEnabled = $auth->isSSOEnabled();
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
// Log error but don't block app
|
||||
error_log("Auth error (app will continue without auth): " . $e->getMessage());
|
||||
}
|
||||
|
||||
// Read and serve the UI HTML
|
||||
$uiContent = file_get_contents(__DIR__ . '/ui.html');
|
||||
|
||||
// Extract the body content from ui.html
|
||||
preg_match('/<body>(.*?)<\/body>/s', $uiContent, $bodyMatches);
|
||||
$bodyContent = $bodyMatches[1] ?? '';
|
||||
|
||||
// Extract the style content from ui.html
|
||||
preg_match('/<style>(.*?)<\/style>/s', $uiContent, $styleMatches);
|
||||
$styleContent = $styleMatches[1] ?? '';
|
||||
|
||||
// Extract the script content from ui.html
|
||||
preg_match_all('/<script[^>]*>(.*?)<\/script>/s', $uiContent, $scriptMatches);
|
||||
$scriptContent = implode("\n", $scriptMatches[1] ?? []);
|
||||
|
||||
// Extract external script sources
|
||||
preg_match_all('/<script[^>]*src="([^"]*)"[^>]*><\/script>/s', $uiContent, $externalScripts);
|
||||
$externalScriptSources = $externalScripts[1] ?? [];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Pencil Automator</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
/* Auth header styles */
|
||||
.auth-header {
|
||||
background: #2c2c2c;
|
||||
padding: 12px 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #444;
|
||||
}
|
||||
.auth-user-info {
|
||||
color: #aaa;
|
||||
font-size: 12px;
|
||||
}
|
||||
.auth-user-name {
|
||||
color: #18A0FB;
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn-logout {
|
||||
background-color: #444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
.btn-logout:hover {
|
||||
background-color: #555;
|
||||
}
|
||||
|
||||
/* Original UI styles */
|
||||
<?php echo $styleContent; ?>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<?php if ($ssoEnabled): ?>
|
||||
<div class="auth-header">
|
||||
<div class="auth-user-info">
|
||||
Logged in as <span class="auth-user-name"><?php echo htmlspecialchars($user['name'] ?? $user['preferred_username'] ?? 'User'); ?></span>
|
||||
</div>
|
||||
<button onclick="signOut()" class="btn-logout">🔒 Log Out</button>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php echo $bodyContent; ?>
|
||||
|
||||
<?php foreach ($externalScriptSources as $src): ?>
|
||||
<script src="<?php echo htmlspecialchars($src); ?>"></script>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<script>
|
||||
<?php if ($ssoEnabled): ?>
|
||||
function signOut() {
|
||||
fetch('auth.php', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'logout' })
|
||||
})
|
||||
.then(() => window.location.reload())
|
||||
.catch(err => console.error('Logout error:', err));
|
||||
}
|
||||
<?php endif; ?>
|
||||
|
||||
<?php echo $scriptContent; ?>
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
5
ui.html
5
ui.html
|
|
@ -2,9 +2,12 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
font-family: 'Inter', sans-serif;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
padding: 20px;
|
||||
background-color: #1e1e1e;
|
||||
color: #ffffff;
|
||||
|
|
|
|||
384
ui.html.backup
Normal file
384
ui.html.backup
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue