Merge pull request #36 from christianlouis/OneDrive
Enhance FTP configuration and OneDrive workflow
This commit is contained in:
@@ -104,6 +104,8 @@ FTP_PORT=21
|
||||
FTP_USERNAME=ftp_user
|
||||
FTP_PASSWORD=your_secure_ftp_password
|
||||
FTP_FOLDER=/Documents/Uploads
|
||||
FTP_USE_TLS=True
|
||||
FTP_ALLOW_PLAINTEXT=True
|
||||
|
||||
# SFTP
|
||||
SFTP_HOST=sftp.example.com
|
||||
|
||||
+370
-3
@@ -1,9 +1,11 @@
|
||||
# app/api.py
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Depends, UploadFile, File
|
||||
from fastapi import APIRouter, Request, HTTPException, status, Depends, UploadFile, File, Form
|
||||
from hashlib import md5
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
import os
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from app.auth import require_login, get_current_user
|
||||
from app.database import SessionLocal
|
||||
@@ -14,8 +16,12 @@ from app.tasks.upload_to_dropbox import upload_to_dropbox
|
||||
from app.tasks.upload_to_paperless import upload_to_paperless
|
||||
from app.tasks.upload_to_nextcloud import upload_to_nextcloud
|
||||
from app.tasks.upload_to_google_drive import upload_to_google_drive
|
||||
from app.tasks.upload_to_onedrive import upload_to_onedrive
|
||||
from app.tasks.send_to_all import send_to_all_destinations
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
def get_db():
|
||||
@@ -186,6 +192,19 @@ def send_to_google_drive(file_path: str):
|
||||
task = upload_to_google_drive.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
@router.post("/send_to_onedrive/")
|
||||
@require_login
|
||||
def send_to_onedrive_endpoint(file_path: str):
|
||||
"""Send a document to OneDrive."""
|
||||
if not os.path.isabs(file_path):
|
||||
file_path = os.path.join(settings.workdir, 'processed', file_path)
|
||||
if not os.path.exists(file_path):
|
||||
raise HTTPException(
|
||||
status_code=400, detail=f"File {file_path} not found."
|
||||
)
|
||||
task = upload_to_onedrive.delay(file_path)
|
||||
return {"task_id": task.id, "status": "queued"}
|
||||
|
||||
@router.post("/send_to_all_destinations/")
|
||||
@require_login
|
||||
def send_to_all_destinations_endpoint(file_path: str):
|
||||
@@ -276,5 +295,353 @@ async def ui_upload(request: Request, file: UploadFile = File(...)):
|
||||
"stored_filename": target_filename
|
||||
}
|
||||
|
||||
# Note: The api/router.py is now a submodule organization,
|
||||
# but we're keeping this file for compatibility until we've fully migrated
|
||||
@router.post("/onedrive/exchange-token")
|
||||
@require_login
|
||||
async def exchange_onedrive_token(
|
||||
request: Request,
|
||||
client_id: str = Form(...),
|
||||
client_secret: str = Form(...),
|
||||
redirect_uri: str = Form(...),
|
||||
code: str = Form(...),
|
||||
tenant_id: str = Form(...)
|
||||
):
|
||||
"""
|
||||
Exchange an authorization code for a refresh token.
|
||||
This is done on the server to avoid exposing client secret in the browser.
|
||||
"""
|
||||
try:
|
||||
logger.info(f"Starting OneDrive token exchange process with tenant_id: {tenant_id}")
|
||||
|
||||
# Prepare the token request
|
||||
token_url = f"https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token"
|
||||
logger.info(f"Using token URL: {token_url}")
|
||||
|
||||
payload = {
|
||||
'client_id': client_id,
|
||||
'scope': 'https://graph.microsoft.com/.default offline_access',
|
||||
'code': code,
|
||||
'redirect_uri': redirect_uri,
|
||||
'grant_type': 'authorization_code',
|
||||
'client_secret': client_secret
|
||||
}
|
||||
|
||||
# Log request details (excluding secret)
|
||||
safe_payload = payload.copy()
|
||||
safe_payload['client_secret'] = '[REDACTED]'
|
||||
safe_payload['code'] = f"{code[:5]}...{code[-5:]}" if len(code) > 10 else '[REDACTED]'
|
||||
logger.info(f"Token exchange request payload: {safe_payload}")
|
||||
|
||||
# Make the token request
|
||||
logger.info("Sending POST request to Microsoft for token exchange")
|
||||
response = requests.post(token_url, data=payload)
|
||||
|
||||
# Check if the request was successful
|
||||
logger.info(f"Token exchange response status: {response.status_code}")
|
||||
|
||||
if response.status_code != 200:
|
||||
# Log the error response for debugging
|
||||
try:
|
||||
error_json = response.json()
|
||||
logger.error(f"Token exchange failed with status {response.status_code}: {error_json}")
|
||||
error_detail = error_json
|
||||
except Exception as json_err:
|
||||
logger.error(f"Failed to parse error response as JSON: {str(json_err)}")
|
||||
logger.error(f"Raw response content: {response.content[:500]}") # Limit log size
|
||||
error_detail = {"error": "Unknown error", "raw_content_snippet": str(response.content[:100])}
|
||||
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Token exchange failed: {error_detail}"
|
||||
)
|
||||
|
||||
# Return the token response
|
||||
token_data = response.json()
|
||||
|
||||
# Validate the token response
|
||||
if "refresh_token" not in token_data:
|
||||
logger.error(f"Microsoft returned success but no refresh token found in response: {token_data.keys()}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="Microsoft OAuth server returned success but no refresh token was included"
|
||||
)
|
||||
|
||||
# Calculate token length for logging
|
||||
refresh_token_length = len(token_data.get("refresh_token", ""))
|
||||
access_token_length = len(token_data.get("access_token", ""))
|
||||
|
||||
logger.info(f"Successfully exchanged authorization code for OneDrive tokens. "
|
||||
f"Refresh token length: {refresh_token_length}, "
|
||||
f"Access token length: {access_token_length}")
|
||||
|
||||
# Return just what's needed by the frontend
|
||||
return {
|
||||
"refresh_token": token_data["refresh_token"],
|
||||
"expires_in": token_data.get("expires_in", 3600)
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
# Re-raise HTTP exceptions as they already have appropriate status codes
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error during OneDrive token exchange: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to exchange token: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/onedrive/test-token")
|
||||
@require_login
|
||||
async def test_onedrive_token(request: Request):
|
||||
"""
|
||||
Test if the configured OneDrive refresh token is valid.
|
||||
Provides detailed error information if token is invalid.
|
||||
"""
|
||||
try:
|
||||
from app.tasks.upload_to_onedrive import get_onedrive_token
|
||||
|
||||
logger.info("Testing OneDrive token validity")
|
||||
if not settings.onedrive_refresh_token:
|
||||
logger.warning("No OneDrive refresh token configured")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "No OneDrive refresh token is configured"
|
||||
}
|
||||
|
||||
# Check if client ID and client secret are configured
|
||||
if not settings.onedrive_client_id or not settings.onedrive_client_secret:
|
||||
logger.warning("OneDrive client ID or client secret is missing")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": "OneDrive client ID or client secret is missing",
|
||||
"missing_config": True
|
||||
}
|
||||
|
||||
# Try to get an access token using the configured refresh token
|
||||
try:
|
||||
access_token = get_onedrive_token()
|
||||
|
||||
# If we got here, token is valid
|
||||
logger.info("OneDrive token is valid")
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "OneDrive token is valid",
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
logger.error(f"OneDrive token test failed: {error_msg}")
|
||||
|
||||
# Determine if this is an invalid_grant error (expired token)
|
||||
is_expired = "invalid_grant" in error_msg.lower()
|
||||
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Token validation failed: {error_msg}",
|
||||
"is_expired": is_expired,
|
||||
"needs_reauth": True
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Unexpected error testing OneDrive token")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": f"Unexpected error: {str(e)}"
|
||||
}
|
||||
|
||||
@router.post("/onedrive/save-settings")
|
||||
@require_login
|
||||
async def save_onedrive_settings(
|
||||
request: Request,
|
||||
client_id: str = Form(None),
|
||||
client_secret: str = Form(None),
|
||||
refresh_token: str = Form(...),
|
||||
tenant_id: str = Form("common"),
|
||||
folder_path: str = Form(None)
|
||||
):
|
||||
"""
|
||||
Save OneDrive settings to the .env file
|
||||
"""
|
||||
try:
|
||||
# Get the path to the .env file
|
||||
env_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), ".env")
|
||||
|
||||
if not os.path.exists(env_path):
|
||||
logger.error(f".env file not found at {env_path}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Could not find .env file to update"
|
||||
)
|
||||
|
||||
logger.info(f"Updating OneDrive settings in {env_path}")
|
||||
|
||||
# Read the current .env file
|
||||
with open(env_path, "r") as f:
|
||||
env_lines = f.readlines()
|
||||
|
||||
# Define settings to update
|
||||
onedrive_settings = {
|
||||
"ONEDRIVE_REFRESH_TOKEN": refresh_token,
|
||||
}
|
||||
|
||||
# Only update these if provided
|
||||
if client_id:
|
||||
onedrive_settings["ONEDRIVE_CLIENT_ID"] = client_id
|
||||
if client_secret:
|
||||
onedrive_settings["ONEDRIVE_CLIENT_SECRET"] = client_secret
|
||||
if tenant_id:
|
||||
onedrive_settings["ONEDRIVE_TENANT_ID"] = tenant_id
|
||||
if folder_path:
|
||||
onedrive_settings["ONEDRIVE_FOLDER_PATH"] = folder_path
|
||||
|
||||
# Process each line and update or add settings
|
||||
updated = set()
|
||||
new_env_lines = []
|
||||
for line in env_lines:
|
||||
line = line.rstrip()
|
||||
is_updated = False
|
||||
for key, value in onedrive_settings.items():
|
||||
if line.startswith(f"{key}=") or line.startswith(f"# {key}="):
|
||||
if line.startswith("# "): # Uncomment if commented out
|
||||
line = line[2:]
|
||||
new_env_lines.append(f"{key}={value}")
|
||||
updated.add(key)
|
||||
is_updated = True
|
||||
break
|
||||
if not is_updated:
|
||||
new_env_lines.append(line)
|
||||
|
||||
# Add any settings that weren't updated (they weren't in the file)
|
||||
for key, value in onedrive_settings.items():
|
||||
if key not in updated:
|
||||
new_env_lines.append(f"{key}={value}")
|
||||
|
||||
# Write the updated .env file
|
||||
with open(env_path, "w") as f:
|
||||
f.write("\n".join(new_env_lines) + "\n")
|
||||
|
||||
# Update the settings in memory
|
||||
if refresh_token:
|
||||
settings.onedrive_refresh_token = refresh_token
|
||||
if client_id:
|
||||
settings.onedrive_client_id = client_id
|
||||
if client_secret:
|
||||
settings.onedrive_client_secret = client_secret
|
||||
if tenant_id:
|
||||
settings.onedrive_tenant_id = tenant_id
|
||||
if folder_path:
|
||||
settings.onedrive_folder_path = folder_path
|
||||
|
||||
logger.info("Successfully updated OneDrive settings")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "OneDrive settings have been saved"
|
||||
}
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error saving OneDrive settings: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to save OneDrive settings: {str(e)}"
|
||||
)
|
||||
|
||||
@router.post("/onedrive/update-settings")
|
||||
@require_login
|
||||
async def update_onedrive_settings(
|
||||
request: Request,
|
||||
client_id: str = Form(None),
|
||||
client_secret: str = Form(None),
|
||||
refresh_token: str = Form(...),
|
||||
tenant_id: str = Form("common"),
|
||||
folder_path: str = Form(None)
|
||||
):
|
||||
"""
|
||||
Update OneDrive settings in memory (without modifying .env file)
|
||||
"""
|
||||
try:
|
||||
logger.info("Updating OneDrive settings in memory")
|
||||
|
||||
# Update settings in memory
|
||||
if refresh_token:
|
||||
settings.onedrive_refresh_token = refresh_token
|
||||
logger.info("Updated ONEDRIVE_REFRESH_TOKEN in memory")
|
||||
|
||||
if client_id:
|
||||
settings.onedrive_client_id = client_id
|
||||
logger.info("Updated ONEDRIVE_CLIENT_ID in memory")
|
||||
|
||||
if client_secret:
|
||||
settings.onedrive_client_secret = client_secret
|
||||
logger.info("Updated ONEDRIVE_CLIENT_SECRET in memory")
|
||||
|
||||
if tenant_id:
|
||||
settings.onedrive_tenant_id = tenant_id
|
||||
logger.info("Updated ONEDRIVE_TENANT_ID in memory")
|
||||
|
||||
if folder_path:
|
||||
settings.onedrive_folder_path = folder_path
|
||||
logger.info("Updated ONEDRIVE_FOLDER_PATH in memory")
|
||||
|
||||
# Test the token to make sure it works
|
||||
try:
|
||||
from app.tasks.upload_to_onedrive import get_onedrive_token
|
||||
access_token = get_onedrive_token()
|
||||
logger.info("Successfully tested OneDrive token")
|
||||
except Exception as e:
|
||||
logger.error(f"Token test failed after updating settings: {str(e)}")
|
||||
return {
|
||||
"status": "warning",
|
||||
"message": "Settings updated but token test failed: " + str(e)
|
||||
}
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"message": "OneDrive settings have been updated in memory"
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Unexpected error updating OneDrive settings: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=f"Failed to update OneDrive settings: {str(e)}"
|
||||
)
|
||||
|
||||
@router.get("/onedrive/get-full-config")
|
||||
@require_login
|
||||
async def get_onedrive_full_config(request: Request):
|
||||
"""
|
||||
Get the full OneDrive configuration for sharing with worker nodes
|
||||
"""
|
||||
try:
|
||||
# Create a configuration object with all OneDrive settings
|
||||
config = {
|
||||
"client_id": settings.onedrive_client_id or "",
|
||||
"client_secret": settings.onedrive_client_secret or "",
|
||||
"tenant_id": settings.onedrive_tenant_id or "common",
|
||||
"refresh_token": settings.onedrive_refresh_token or "",
|
||||
"folder_path": settings.onedrive_folder_path or "Documents/Uploads"
|
||||
}
|
||||
|
||||
# Generate environment variable format
|
||||
env_format = "\n".join([
|
||||
f"ONEDRIVE_CLIENT_ID={config['client_id']}",
|
||||
f"ONEDRIVE_CLIENT_SECRET={config['client_secret']}",
|
||||
f"ONEDRIVE_TENANT_ID={config['tenant_id']}",
|
||||
f"ONEDRIVE_REFRESH_TOKEN={config['refresh_token']}",
|
||||
f"ONEDRIVE_FOLDER_PATH={config['folder_path']}"
|
||||
])
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"config": config,
|
||||
"env_format": env_format
|
||||
}
|
||||
except Exception as e:
|
||||
logger.exception("Error getting OneDrive configuration")
|
||||
return {
|
||||
"status": "error",
|
||||
"message": str(e)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -77,6 +77,8 @@ class Settings(BaseSettings):
|
||||
ftp_username: Optional[str] = None
|
||||
ftp_password: Optional[str] = None
|
||||
ftp_folder: Optional[str] = None
|
||||
ftp_use_tls: bool = True # Default to attempting TLS connection first
|
||||
ftp_allow_plaintext: bool = True # Default to allowing plaintext fallback
|
||||
|
||||
# SFTP settings
|
||||
sftp_host: Optional[str] = None
|
||||
|
||||
+64
-2
@@ -4,6 +4,7 @@ from fastapi.templating import Jinja2Templates
|
||||
from pathlib import Path
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime
|
||||
import logging
|
||||
|
||||
from app.auth import require_login
|
||||
from app.database import SessionLocal
|
||||
@@ -15,6 +16,9 @@ router = APIRouter()
|
||||
templates_dir = Path(__file__).parent.parent / "frontend" / "templates"
|
||||
templates = Jinja2Templates(directory=str(templates_dir))
|
||||
|
||||
# Set up logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -95,5 +99,63 @@ async def env_debug(request: Request):
|
||||
}
|
||||
)
|
||||
|
||||
# Note: The frontend/router.py is now a submodule organization,
|
||||
# but we're keeping this file for compatibility until we've fully migrated
|
||||
@router.get("/onedrive-setup")
|
||||
@require_login
|
||||
async def onedrive_setup_page(request: Request):
|
||||
"""
|
||||
Setup page for the OneDrive integration.
|
||||
Shows configuration status and setup instructions.
|
||||
"""
|
||||
# Check OneDrive configuration
|
||||
is_configured = bool(settings.onedrive_client_id and
|
||||
settings.onedrive_client_secret and
|
||||
settings.onedrive_refresh_token)
|
||||
|
||||
# Get configuration values to display status (hide sensitive values)
|
||||
return templates.TemplateResponse(
|
||||
"onedrive.html",
|
||||
{
|
||||
"request": request,
|
||||
"is_configured": is_configured,
|
||||
"client_id": bool(settings.onedrive_client_id),
|
||||
"client_id_value": settings.onedrive_client_id or "", # Pass the actual value for the form
|
||||
"client_secret": bool(settings.onedrive_client_secret),
|
||||
"client_secret_value": settings.onedrive_client_secret if settings.onedrive_client_secret else "",
|
||||
"tenant_id": settings.onedrive_tenant_id,
|
||||
"refresh_token": bool(settings.onedrive_refresh_token),
|
||||
"refresh_token_value": settings.onedrive_refresh_token if settings.onedrive_refresh_token else "",
|
||||
"folder_path": settings.onedrive_folder_path or "Documents/Uploads" # Default folder path
|
||||
}
|
||||
)
|
||||
|
||||
@router.get("/onedrive-callback")
|
||||
@require_login
|
||||
async def onedrive_callback(request: Request, code: str = None, error: str = None):
|
||||
"""
|
||||
Callback endpoint for OneDrive OAuth flow.
|
||||
Now automatically exchanges the code for a token and saves it to the configuration.
|
||||
"""
|
||||
if error:
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback_error.html",
|
||||
{"request": request, "error": error}
|
||||
)
|
||||
|
||||
if not code:
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback_error.html",
|
||||
{"request": request, "error": "No authorization code received from Microsoft"}
|
||||
)
|
||||
|
||||
# Display the processing page with automatic token exchange
|
||||
return templates.TemplateResponse(
|
||||
"onedrive_callback.html",
|
||||
{
|
||||
"request": request,
|
||||
"code": code,
|
||||
"client_id_value": settings.onedrive_client_id or "",
|
||||
"client_secret_value": settings.onedrive_client_secret or "",
|
||||
"tenant_id": settings.onedrive_tenant_id or "common"
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
+61
-12
@@ -26,18 +26,66 @@ def upload_to_ftp(file_path: str):
|
||||
raise ValueError(error_msg)
|
||||
|
||||
try:
|
||||
# Connect to FTP server
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(
|
||||
host=settings.ftp_host,
|
||||
port=settings.ftp_port or 21
|
||||
)
|
||||
# First attempt FTPS (FTP with TLS)
|
||||
use_tls = getattr(settings, 'ftp_use_tls', True) # Default to try TLS
|
||||
allow_plaintext = getattr(settings, 'ftp_allow_plaintext', True) # Default to allow plaintext fallback
|
||||
|
||||
# Login with credentials
|
||||
ftp.login(
|
||||
user=settings.ftp_username,
|
||||
passwd=settings.ftp_password
|
||||
)
|
||||
if use_tls:
|
||||
try:
|
||||
logger.info(f"Attempting FTPS connection to {settings.ftp_host}")
|
||||
ftp = ftplib.FTP_TLS()
|
||||
ftp.connect(
|
||||
host=settings.ftp_host,
|
||||
port=settings.ftp_port or 21
|
||||
)
|
||||
|
||||
# Login with credentials
|
||||
ftp.login(
|
||||
user=settings.ftp_username,
|
||||
passwd=settings.ftp_password
|
||||
)
|
||||
|
||||
# Enable data protection - encrypt the data channel
|
||||
ftp.prot_p()
|
||||
logger.info("Successfully established FTPS connection with TLS")
|
||||
except Exception as e:
|
||||
if not allow_plaintext:
|
||||
error_msg = f"FTPS connection failed and plaintext FTP is forbidden: {str(e)}"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
else:
|
||||
logger.warning(f"FTPS connection failed, falling back to regular FTP: {str(e)}")
|
||||
# Fall back to regular FTP
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(
|
||||
host=settings.ftp_host,
|
||||
port=settings.ftp_port or 21
|
||||
)
|
||||
|
||||
# Login with credentials
|
||||
ftp.login(
|
||||
user=settings.ftp_username,
|
||||
passwd=settings.ftp_password
|
||||
)
|
||||
else:
|
||||
# Check if plaintext is allowed when TLS is explicitly disabled
|
||||
if not allow_plaintext:
|
||||
error_msg = "Plaintext FTP is forbidden by configuration"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
# Directly use regular FTP if TLS is explicitly disabled
|
||||
ftp = ftplib.FTP()
|
||||
ftp.connect(
|
||||
host=settings.ftp_host,
|
||||
port=settings.ftp_port or 21
|
||||
)
|
||||
|
||||
# Login with credentials
|
||||
ftp.login(
|
||||
user=settings.ftp_username,
|
||||
passwd=settings.ftp_password
|
||||
)
|
||||
|
||||
# Change to target directory if specified
|
||||
if settings.ftp_folder:
|
||||
@@ -80,7 +128,8 @@ def upload_to_ftp(file_path: str):
|
||||
"status": "Completed",
|
||||
"file": file_path,
|
||||
"ftp_host": settings.ftp_host,
|
||||
"ftp_path": f"{settings.ftp_folder}/{filename}" if settings.ftp_folder else filename
|
||||
"ftp_path": f"{settings.ftp_folder}/{filename}" if settings.ftp_folder else filename,
|
||||
"used_tls": isinstance(ftp, ftplib.FTP_TLS)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
|
||||
@@ -21,21 +21,52 @@ def get_onedrive_token():
|
||||
if not settings.onedrive_client_id or not settings.onedrive_client_secret:
|
||||
raise ValueError("OneDrive client ID and client secret must be configured")
|
||||
|
||||
# Log more details about the configuration
|
||||
tenant = settings.onedrive_tenant_id or "common"
|
||||
logger.info(f"Using OneDrive tenant: {tenant}")
|
||||
|
||||
# Define scopes consistently
|
||||
scopes = ["https://graph.microsoft.com/.default"]
|
||||
|
||||
# Use refresh token flow (works for both personal and org accounts)
|
||||
if settings.onedrive_refresh_token:
|
||||
# Use MSAL to get token from refresh token
|
||||
app = msal.PublicClientApplication(settings.onedrive_client_id)
|
||||
# Use MSAL's ConfidentialClientApplication instead of PublicClientApplication
|
||||
app = msal.ConfidentialClientApplication(
|
||||
client_id=settings.onedrive_client_id,
|
||||
client_credential=settings.onedrive_client_secret,
|
||||
authority=f"https://login.microsoftonline.com/{tenant}"
|
||||
)
|
||||
|
||||
# Request new token using refresh token
|
||||
logger.info("Attempting to acquire token using refresh token")
|
||||
token_response = app.acquire_token_by_refresh_token(
|
||||
refresh_token=settings.onedrive_refresh_token,
|
||||
scopes=["https://graph.microsoft.com/Files.ReadWrite"]
|
||||
scopes=scopes
|
||||
)
|
||||
|
||||
if "access_token" not in token_response:
|
||||
error = token_response.get("error", "")
|
||||
error_desc = token_response.get("error_description", "Unknown error")
|
||||
|
||||
# Log more details about the error
|
||||
logger.error(f"Failed to get access token using refresh token")
|
||||
logger.error(f"Error code: {error}")
|
||||
logger.error(f"Error description: {error_desc}")
|
||||
|
||||
if error == "invalid_grant":
|
||||
logger.error("The refresh token appears to be expired or revoked")
|
||||
logger.error("A new authorization flow is required to obtain a fresh token")
|
||||
|
||||
raise ValueError(f"Failed to get access token: {error} - {error_desc}")
|
||||
|
||||
# Check if we received a new refresh token and update it
|
||||
if "refresh_token" in token_response:
|
||||
new_refresh_token = token_response["refresh_token"]
|
||||
logger.info("Received new refresh token from Microsoft")
|
||||
|
||||
# Update the refresh token in memory
|
||||
settings.onedrive_refresh_token = new_refresh_token
|
||||
logger.info("Updated refresh token in memory")
|
||||
|
||||
return token_response["access_token"]
|
||||
|
||||
@@ -50,7 +81,7 @@ def get_onedrive_token():
|
||||
|
||||
# Acquire token for application
|
||||
token_response = app.acquire_token_for_client(
|
||||
scopes=["https://graph.microsoft.com/.default"]
|
||||
scopes=scopes
|
||||
)
|
||||
|
||||
if "access_token" not in token_response:
|
||||
|
||||
@@ -238,6 +238,21 @@ def get_provider_status():
|
||||
"folder": getattr(settings, 'webdav_folder', 'Not set')
|
||||
}
|
||||
}
|
||||
|
||||
# Add FTP configuration to providers
|
||||
providers["FTP Storage"] = {
|
||||
"name": "FTP Storage",
|
||||
"configured": bool(getattr(settings, 'ftp_host', None) and
|
||||
getattr(settings, 'ftp_username', None) and
|
||||
getattr(settings, 'ftp_password', None)),
|
||||
"enabled": True,
|
||||
"details": {
|
||||
"host": getattr(settings, 'ftp_host', 'Not set'),
|
||||
"folder": getattr(settings, 'ftp_folder', 'Not set'),
|
||||
"tls": getattr(settings, 'ftp_use_tls', True),
|
||||
"allow_plaintext": getattr(settings, 'ftp_allow_plaintext', True)
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
|
||||
@@ -363,7 +378,9 @@ def get_settings_for_display(show_values=False):
|
||||
"ftp_port",
|
||||
"ftp_username",
|
||||
"ftp_password",
|
||||
"ftp_folder"
|
||||
"ftp_folder",
|
||||
"ftp_use_tls",
|
||||
"ftp_allow_plaintext"
|
||||
],
|
||||
"S3/AWS": [
|
||||
"aws_access_key_id",
|
||||
|
||||
@@ -4,6 +4,7 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: document_api
|
||||
restart: always
|
||||
|
||||
# We'll keep the code in /app, but set working_dir to the shared data directory
|
||||
working_dir: /workdir
|
||||
@@ -34,6 +35,7 @@ services:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
container_name: document_worker
|
||||
restart: always
|
||||
|
||||
# same shared working directory
|
||||
working_dir: /workdir
|
||||
@@ -55,6 +57,7 @@ services:
|
||||
gotenberg:
|
||||
image: gotenberg/gotenberg:latest
|
||||
container_name: gotenberg
|
||||
restart: always
|
||||
|
||||
|
||||
redis:
|
||||
|
||||
@@ -102,6 +102,8 @@ For detailed setup instructions, see the [Google Drive Setup Guide](GoogleDriveS
|
||||
| `FTP_USERNAME` | FTP authentication username. |
|
||||
| `FTP_PASSWORD` | FTP authentication password. |
|
||||
| `FTP_FOLDER` | Destination folder on FTP server (e.g. `"/Documents/Uploads"`). |
|
||||
| `FTP_USE_TLS` | Try to use FTPS with TLS encryption first (default: `True`). |
|
||||
| `FTP_ALLOW_PLAINTEXT` | Allow fallback to plaintext FTP if TLS fails (default: `True`). |
|
||||
|
||||
### SFTP
|
||||
|
||||
@@ -235,6 +237,8 @@ FTP_PORT=21
|
||||
FTP_USERNAME=username
|
||||
FTP_PASSWORD=password
|
||||
FTP_FOLDER=/Documents/Uploads
|
||||
FTP_USE_TLS=True
|
||||
FTP_ALLOW_PLAINTEXT=True
|
||||
|
||||
# SFTP
|
||||
SFTP_HOST=sftp.example.com
|
||||
|
||||
+61
-20
@@ -14,6 +14,27 @@ This guide explains how to set up the Microsoft OneDrive integration for DocuNov
|
||||
|
||||
For a complete list of configuration options, see the [Configuration Guide](ConfigurationGuide.md).
|
||||
|
||||
## Setup Methods
|
||||
|
||||
You can set up OneDrive integration in two ways:
|
||||
|
||||
1. **Using the Auth Wizard (Recommended)**: An interactive setup experience available at `/onedrive-setup` in the web interface
|
||||
2. **Manual Setup**: Following the step-by-step instructions in this document
|
||||
|
||||
## Using the Auth Wizard
|
||||
|
||||
The easiest way to set up OneDrive integration is to use the built-in auth wizard:
|
||||
|
||||
1. Register an application in Azure AD (see steps below)
|
||||
2. Navigate to the OneDrive Setup page at `/onedrive-setup`
|
||||
3. Enter your Client ID and other required information
|
||||
4. Click "Start Authentication Flow"
|
||||
5. Complete the Microsoft authentication process
|
||||
6. The system will automatically exchange the authorization code for a refresh token
|
||||
7. Copy the generated environment variables for your worker nodes
|
||||
|
||||
The auth wizard handles all the token exchange steps and provides you with the exact configuration needed for your environment.
|
||||
|
||||
## Types of Microsoft Accounts
|
||||
|
||||
There are two main types of Microsoft accounts that can be used with OneDrive integration:
|
||||
@@ -34,7 +55,9 @@ The setup process differs slightly based on which account type you're using.
|
||||
5. For "Supported account types", select the appropriate option:
|
||||
- For personal accounts: "Accounts in any organizational directory and personal Microsoft accounts"
|
||||
- For corporate accounts only: "Accounts in this organizational directory only"
|
||||
6. For Redirect URI, select "Web" and enter a URL you can access (e.g., `http://localhost:8000/auth/callback`)
|
||||
6. For Redirect URI, select "Web" and enter your callback URL:
|
||||
- For auth wizard: `https://your-domain.com/onedrive-callback`
|
||||
- For manual setup: any URL you can access (e.g., `http://localhost:8000/auth/callback`)
|
||||
7. Click "Register"
|
||||
|
||||
### 2. Get Application (client) ID
|
||||
@@ -50,6 +73,21 @@ The setup process differs slightly based on which account type you're using.
|
||||
4. Click "Add" and immediately copy the secret value (it will only be shown once)
|
||||
5. Set this value as `ONEDRIVE_CLIENT_SECRET`
|
||||
|
||||
### 4. Configure API permissions
|
||||
|
||||
1. In your application page, go to "API permissions"
|
||||
2. Click "Add a permission"
|
||||
3. Select "Microsoft Graph" > "Delegated permissions"
|
||||
4. Search for and add the following permission:
|
||||
- `Files.ReadWrite` (Allows the app to read and write files that the user has access to)
|
||||
- `offline_access` (Required for refresh tokens)
|
||||
5. If using application permissions (for service accounts), add:
|
||||
- `Files.ReadWrite.All` (Required for app-only access)
|
||||
6. Click "Add permissions"
|
||||
7. For organizational accounts, you may need an admin to "Grant admin consent"
|
||||
|
||||
> **Important Scope Change**: The system now uses the `.default` scope during authentication, which requests all permissions that have been granted to the application. This ensures that the app has all required permissions at once, rather than requesting each scope individually.
|
||||
|
||||
## For Personal Microsoft Accounts
|
||||
|
||||
If you're using a personal Microsoft account (@outlook.com, @hotmail.com, or personal accounts linked to other emails):
|
||||
@@ -58,34 +96,31 @@ If you're using a personal Microsoft account (@outlook.com, @hotmail.com, or per
|
||||
|
||||
- Set `ONEDRIVE_TENANT_ID=common` in your configuration
|
||||
|
||||
### 2. Configure API permissions
|
||||
### 2. Generate a Refresh Token
|
||||
|
||||
1. In your application page, go to "API permissions"
|
||||
2. Click "Add a permission"
|
||||
3. Select "Microsoft Graph" > "Delegated permissions"
|
||||
4. Search for and add the following permissions:
|
||||
- `Files.ReadWrite` (Allows the app to read and write files that the user has access to)
|
||||
- `offline_access` (Needed for refresh tokens)
|
||||
5. Click "Add permissions"
|
||||
|
||||
### 3. Generate a Refresh Token
|
||||
#### Using the Auth Wizard (Recommended)
|
||||
1. Navigate to the OneDrive Setup page at `/onedrive-setup`
|
||||
2. Enter your Client ID and leave Tenant ID as "common"
|
||||
3. Click "Start Authentication Flow" and follow the prompts
|
||||
4. The wizard will handle token exchange automatically
|
||||
|
||||
#### Manual Method
|
||||
1. Use the following URL (replace CLIENT_ID and REDIRECT_URI with your values):
|
||||
```
|
||||
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&response_mode=query&scope=offline_access%20Files.ReadWrite
|
||||
https://login.microsoftonline.com/common/oauth2/v2.0/authorize?client_id=YOUR_CLIENT_ID&response_type=code&redirect_uri=YOUR_REDIRECT_URI&response_mode=query&scope=https://graph.microsoft.com/.default offline_access&prompt=consent
|
||||
```
|
||||
2. Open this URL in your browser
|
||||
3. Sign in with your personal Microsoft account
|
||||
4. After authentication, you'll be redirected to your redirect URI with a code parameter in the URL
|
||||
5. Copy the code value from the URL (everything after "code=")
|
||||
|
||||
### 4. Exchange Code for Refresh Token
|
||||
### 3. Exchange Code for Refresh Token (Manual Method Only)
|
||||
|
||||
1. Use the following command to exchange the code for tokens:
|
||||
```bash
|
||||
curl -X POST https://login.microsoftonline.com/common/oauth2/v2.0/token \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-d "client_id=YOUR_CLIENT_ID&scope=offline_access Files.ReadWrite&code=YOUR_AUTH_CODE&redirect_uri=YOUR_REDIRECT_URI&grant_type=authorization_code&client_secret=YOUR_CLIENT_SECRET"
|
||||
-d "client_id=YOUR_CLIENT_ID&scope=https://graph.microsoft.com/.default offline_access&code=YOUR_AUTH_CODE&redirect_uri=YOUR_REDIRECT_URI&grant_type=authorization_code&client_secret=YOUR_CLIENT_SECRET"
|
||||
```
|
||||
2. From the response JSON, copy the `refresh_token` value
|
||||
3. Set this as `ONEDRIVE_REFRESH_TOKEN` in your configuration
|
||||
@@ -104,10 +139,11 @@ If you're using a work/school account provided by your organization:
|
||||
|
||||
**Option A: Access your own OneDrive (Interactive Login)**
|
||||
|
||||
This option requires a refresh token just like personal accounts:
|
||||
1. Follow the same steps as for personal accounts, but use your work email to sign in
|
||||
2. Make sure to set `ONEDRIVE_TENANT_ID` to your organization's tenant ID instead of "common"
|
||||
3. Set the refresh token you receive as `ONEDRIVE_REFRESH_TOKEN`
|
||||
This option requires a refresh token:
|
||||
1. Use the auth wizard with your tenant ID, or
|
||||
2. Follow the same manual steps as for personal accounts, but use your work email to sign in
|
||||
3. Make sure to set `ONEDRIVE_TENANT_ID` to your organization's tenant ID instead of "common"
|
||||
4. Set the refresh token you receive as `ONEDRIVE_REFRESH_TOKEN`
|
||||
|
||||
**Option B: Access OneDrive as a system service (App-only access)**
|
||||
|
||||
@@ -131,8 +167,13 @@ If you encounter errors during authentication:
|
||||
- For corporate accounts, ensure an admin has consented to the permissions
|
||||
|
||||
3. **Refresh token expired**:
|
||||
- If uploads stop working, you may need to generate a new refresh token
|
||||
- Repeat the process to get a new authorization code and refresh token
|
||||
- If uploads stop working, you can generate a new refresh token using the auth wizard
|
||||
- Click on "Refresh Token" in the OneDrive setup page
|
||||
|
||||
4. **Scope issues**:
|
||||
- If you see permission errors, make sure your app has the correct permissions added
|
||||
- The `.default` scope is now used, which includes all permissions granted to the app
|
||||
- Check that both `Files.ReadWrite` and `offline_access` permissions are added to your app
|
||||
|
||||
## Configuration Examples
|
||||
|
||||
|
||||
@@ -31,15 +31,54 @@
|
||||
</div>
|
||||
|
||||
<!-- Menu Items -->
|
||||
<div class="flex space-x-4 items-center">
|
||||
<div class="hidden md:flex space-x-4 items-center">
|
||||
<a href="/" class="text-gray-700 hover:text-gray-900">Home</a>
|
||||
<a href="/upload" class="text-gray-700 hover:text-gray-900">Upload</a>
|
||||
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
|
||||
<a href="/files" class="text-gray-700 hover:text-gray-900">Files</a>
|
||||
<a href="/status" class="text-gray-700 hover:text-gray-900">Status</a>
|
||||
|
||||
<!-- Integrations Dropdown -->
|
||||
<div class="relative inline-block text-left" x-data="{ open: false }">
|
||||
<button @click="open = !open" class="text-gray-700 hover:text-gray-900 inline-flex items-center">
|
||||
Integrations
|
||||
<svg class="ml-1 h-5 w-5" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<div
|
||||
x-show="open"
|
||||
@click.away="open = false"
|
||||
class="origin-top-right absolute right-0 mt-2 w-48 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
x-transition:enter="transition ease-out duration-100"
|
||||
x-transition:enter-start="transform opacity-0 scale-95"
|
||||
x-transition:enter-end="transform opacity-100 scale-100"
|
||||
x-transition:leave="transition ease-in duration-75"
|
||||
x-transition:leave-start="transform opacity-100 scale-100"
|
||||
x-transition:leave-end="transform opacity-0 scale-95"
|
||||
>
|
||||
<div class="py-1">
|
||||
<a href="/onedrive-setup" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900">OneDrive Setup</a>
|
||||
<!-- Add other integration setup links here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a href="/about" class="text-gray-700 hover:text-gray-900">About</a>
|
||||
|
||||
<!-- Dynamic Auth Section -->
|
||||
<div id="authSection" class="text-gray-700 hover:text-gray-900"></div>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<div class="md:hidden">
|
||||
<button type="button" class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500" aria-expanded="false">
|
||||
<span class="sr-only">Open main menu</span>
|
||||
<svg class="block h-6 w-6" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -55,6 +94,8 @@
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Alpine.js for dropdown functionality -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js" defer></script>
|
||||
<!-- Common JS (shared) -->
|
||||
<script src="/static/js/common.js"></script>
|
||||
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}OneDrive Setup{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="mb-8">
|
||||
<h1 class="text-3xl font-bold mb-2">OneDrive Integration Setup</h1>
|
||||
<p class="text-gray-600 mb-4">
|
||||
Configure the Microsoft OneDrive integration for DocuNova using our setup wizard.
|
||||
</p>
|
||||
|
||||
<div class="bg-blue-100 border-l-4 border-blue-500 text-blue-700 p-4 my-4" role="alert">
|
||||
<p class="font-bold">Current Status:</p>
|
||||
<p>OneDrive integration is
|
||||
{% if is_configured %}
|
||||
<span class="text-green-700 font-semibold">configured</span>.
|
||||
{% else %}
|
||||
<span class="text-red-700 font-semibold">not configured</span>.
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if folder_path %}
|
||||
<p class="mt-2"><strong>Target folder:</strong> {{ folder_path }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 my-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm text-yellow-700">
|
||||
<strong>Note:</strong> You only need to create an Azure application and get credentials. The setup wizard will handle the rest automatically.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">Quick Setup Guide</h2>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 1: Register an Azure Application</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>Go to the <a href="https://portal.azure.com/" target="_blank" class="text-blue-600 hover:underline">Azure Portal</a></li>
|
||||
<li>Navigate to "Azure Active Directory" > "App registrations"</li>
|
||||
<li>Click "New registration"</li>
|
||||
<li>Enter a name for your application (e.g., "DocuNova")</li>
|
||||
<li>For "Supported account types", select:
|
||||
<div class="bg-gray-50 p-2 my-2 rounded text-sm">
|
||||
"Accounts in any organizational directory and personal Microsoft accounts"
|
||||
</div>
|
||||
</li>
|
||||
<li>For Redirect URI:
|
||||
<ul class="list-disc ml-6 mt-1">
|
||||
<li>Select "Web"</li>
|
||||
<li>Enter: <code class="bg-gray-100 p-1">{{ request.url.scheme }}://{{ request.url.netloc }}/onedrive-callback</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click "Register"</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 2: Configure API Permissions</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>In your application page, go to "API permissions"</li>
|
||||
<li>Click "Add a permission"</li>
|
||||
<li>Select "Microsoft Graph" > "Delegated permissions"</li>
|
||||
<li>Search for and add these permissions:
|
||||
<ul class="list-disc ml-6 mt-1">
|
||||
<li><code class="bg-gray-100 p-1">Files.ReadWrite</code></li>
|
||||
<li><code class="bg-gray-100 p-1">offline_access</code></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>Click "Add permissions"</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<h3 class="text-xl font-medium mb-4">Step 3: Get Client ID and Create Secret</h3>
|
||||
<ol class="list-decimal ml-6 space-y-3">
|
||||
<li>Copy the "Application (client) ID" from the overview page</li>
|
||||
<li>Go to "Certificates & secrets"</li>
|
||||
<li>Under "Client secrets", click "New client secret"</li>
|
||||
<li>Add a description and select an expiration period</li>
|
||||
<li>Click "Add" and <strong>immediately copy the secret value</strong> (it will only be shown once)</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
|
||||
<h2 class="text-2xl font-semibold mb-4">Complete Setup with Wizard</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="client-id" class="block text-sm font-medium text-gray-700">Client ID</label>
|
||||
<input type="text" id="client-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Azure AD application client ID" value="{{ client_id_value }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="client-secret" class="block text-sm font-medium text-gray-700">Client Secret</label>
|
||||
<input type="password" id="client-secret" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="Enter your Azure AD application client secret" value="{{ client_secret_value }}">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="tenant-id" class="block text-sm font-medium text-gray-700">Tenant ID (Optional)</label>
|
||||
<input type="text" id="tenant-id" class="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm" placeholder="common" value="{{ tenant_id }}">
|
||||
<p class="text-xs text-gray-500 mt-1">Use "common" for personal accounts or your organization's Tenant ID for corporate accounts</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button id="start-auth-flow" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Start Authentication Flow
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Token validation and status -->
|
||||
<div id="token-status" class="mt-6 {{ 'hidden' if not is_configured else '' }}">
|
||||
<div class="rounded-md {{ 'bg-green-50' if is_configured else 'bg-yellow-50' }} p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
{% if is_configured %}
|
||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% else %}
|
||||
<svg class="h-5 w-5 text-yellow-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium {{ 'text-green-800' if is_configured else 'text-yellow-800' }}">
|
||||
{% if is_configured %}
|
||||
OneDrive integration is properly configured! Your refresh token is valid.
|
||||
{% else %}
|
||||
OneDrive integration is not completely configured or token may be invalid.
|
||||
{% endif %}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 flex space-x-3">
|
||||
<button id="test-token" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Test Token
|
||||
</button>
|
||||
<button id="refresh-token-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md shadow-sm text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
|
||||
Refresh Token
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Configuration for Worker Nodes section -->
|
||||
<div class="mt-6 p-4 bg-gray-100 rounded-md">
|
||||
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Copy these environment variables to configure all worker nodes:
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code>ONEDRIVE_CLIENT_ID={{ client_id_value }}
|
||||
ONEDRIVE_CLIENT_SECRET={{ client_secret_value|default('YOUR_CLIENT_SECRET', true) }}
|
||||
ONEDRIVE_TENANT_ID={{ tenant_id }}
|
||||
ONEDRIVE_REFRESH_TOKEN={{ refresh_token_value|default('YOUR_REFRESH_TOKEN', true) }}
|
||||
ONEDRIVE_FOLDER_PATH={{ folder_path|default('Documents/Uploads', true) }}</code></pre>
|
||||
|
||||
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Add these variables to your .env file or environment configuration.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white shadow-md rounded-lg p-6 mb-8">
|
||||
<div class="flex items-start">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-6 w-6 text-blue-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-lg font-medium">Need More Information?</h3>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
For detailed instructions and troubleshooting, refer to the
|
||||
<a href="/docs/OneDriveSetup.md" target="_blank" class="text-blue-600 hover:underline">OneDrive Setup Documentation</a>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-8">
|
||||
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Back to Status
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Elements
|
||||
const startAuthFlowBtn = document.getElementById('start-auth-flow');
|
||||
const testTokenBtn = document.getElementById('test-token');
|
||||
const refreshTokenBtn = document.getElementById('refresh-token-btn');
|
||||
const tokenStatus = document.getElementById('token-status');
|
||||
const clientSecretInput = document.getElementById('client-secret');
|
||||
|
||||
// Start Authentication Flow button click
|
||||
startAuthFlowBtn.addEventListener('click', function() {
|
||||
const clientId = document.getElementById('client-id').value.trim();
|
||||
const clientSecret = clientSecretInput.value.trim();
|
||||
const redirectUri = window.location.origin + "/onedrive-callback";
|
||||
const tenantId = document.getElementById('tenant-id').value.trim() || 'common';
|
||||
|
||||
if (!clientId) {
|
||||
alert('Please enter your Client ID');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!clientSecret) {
|
||||
alert('Please enter your Client Secret');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save client secret to session storage temporarily
|
||||
sessionStorage.setItem('onedrive_client_secret', clientSecret);
|
||||
|
||||
// Generate the authorization URL with .default scope
|
||||
const authUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?client_id=${encodeURIComponent(clientId)}&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&response_mode=query&scope=${encodeURIComponent('https://graph.microsoft.com/.default offline_access')}&prompt=consent`;
|
||||
|
||||
// Redirect the user to the Microsoft login page
|
||||
window.location.href = authUrl;
|
||||
});
|
||||
|
||||
// Test Token button click
|
||||
if (testTokenBtn) {
|
||||
testTokenBtn.addEventListener('click', function() {
|
||||
testTokenBtn.innerHTML = '<span class="animate-spin inline-block mr-2">⟳</span> Testing...';
|
||||
testTokenBtn.disabled = true;
|
||||
|
||||
fetch('/api/onedrive/test-token')
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
if (data.status === 'success') {
|
||||
alert('Token is valid! Successfully connected to OneDrive.');
|
||||
} else {
|
||||
if (data.message && data.message.includes('invalid_grant')) {
|
||||
if (confirm('Your OneDrive token has expired or is invalid. Would you like to obtain a new token now?')) {
|
||||
startAuthFlowBtn.click();
|
||||
}
|
||||
} else {
|
||||
alert('Token validation failed: ' + data.message);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('Error testing token: ' + error.message);
|
||||
})
|
||||
.finally(() => {
|
||||
testTokenBtn.innerHTML = 'Test Token';
|
||||
testTokenBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh Token button click
|
||||
if (refreshTokenBtn) {
|
||||
refreshTokenBtn.addEventListener('click', function() {
|
||||
if (confirm('This will start a new authentication flow to obtain a fresh token from Microsoft. Continue?')) {
|
||||
startAuthFlowBtn.click();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Copy Environment Variables Button
|
||||
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
|
||||
if (copyEnvVarsBtn) {
|
||||
copyEnvVarsBtn.addEventListener('click', function() {
|
||||
const envVarsText = document.getElementById('env-vars').textContent;
|
||||
navigator.clipboard.writeText(envVarsText)
|
||||
.then(() => {
|
||||
const originalText = copyEnvVarsBtn.textContent;
|
||||
copyEnvVarsBtn.textContent = 'Copied!';
|
||||
copyEnvVarsBtn.classList.add('bg-green-700');
|
||||
setTimeout(() => {
|
||||
copyEnvVarsBtn.textContent = originalText;
|
||||
copyEnvVarsBtn.classList.remove('bg-green-700');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy text to clipboard');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Try to retrieve client secret from session storage (if coming back from auth)
|
||||
if (clientSecretInput && !clientSecretInput.value && sessionStorage.getItem('onedrive_client_secret')) {
|
||||
clientSecretInput.value = sessionStorage.getItem('onedrive_client_secret');
|
||||
// Clear it after use
|
||||
sessionStorage.removeItem('onedrive_client_secret');
|
||||
}
|
||||
|
||||
// If token is not configured but we have a client ID, show the token status section
|
||||
if (document.getElementById('client-id').value && !tokenStatus.classList.contains('hidden')) {
|
||||
tokenStatus.classList.remove('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,215 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}OneDrive Authorization Processing{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="bg-white shadow-md rounded-lg p-6">
|
||||
<div class="text-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mt-4">Processing Authorization</h2>
|
||||
<p class="text-gray-600 mt-2">Please wait while we complete the OneDrive authorization process...</p>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center my-6">
|
||||
<div class="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-500"></div>
|
||||
</div>
|
||||
|
||||
<div id="processing-message" class="text-center text-gray-700">
|
||||
<p>Exchanging authorization code for refresh token...</p>
|
||||
</div>
|
||||
|
||||
<div id="error-container" class="hidden mt-6">
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
|
||||
<div class="mt-2 text-sm text-red-700" id="error-message">
|
||||
<!-- Error message will be inserted here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<a href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Return to Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="success-container" class="hidden mt-6">
|
||||
<div class="rounded-md bg-green-50 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<p class="text-sm font-medium text-green-800">
|
||||
OneDrive authorization successful! Your refresh token has been saved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 p-4 bg-gray-100 rounded-md">
|
||||
<h3 class="font-medium text-lg mb-2">Configuration for Worker Nodes</h3>
|
||||
<p class="text-sm text-gray-600 mb-3">
|
||||
Copy these environment variables to configure all worker nodes:
|
||||
</p>
|
||||
|
||||
<div class="relative">
|
||||
<pre id="env-vars" class="bg-gray-800 text-green-400 text-sm p-3 rounded overflow-x-auto"><code></code></pre>
|
||||
|
||||
<button id="copy-env-vars" class="absolute top-2 right-2 bg-gray-700 hover:bg-gray-600 text-white text-xs py-1 px-2 rounded">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-gray-500 mt-2">
|
||||
Add these variables to your .env file or environment configuration.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 text-center">
|
||||
<a href="/status" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Go to Status Page
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const code = "{{ code }}";
|
||||
const clientId = "{{ client_id_value }}";
|
||||
const redirectUri = window.location.origin + "/onedrive-callback";
|
||||
const tenantId = "{{ tenant_id }}";
|
||||
|
||||
// Automatically exchange the code for a refresh token
|
||||
if (code) {
|
||||
exchangeCode(code, clientId, redirectUri, tenantId);
|
||||
} else {
|
||||
showError("No authorization code was found in the URL");
|
||||
}
|
||||
|
||||
function exchangeCode(code, clientId, redirectUri, tenantId) {
|
||||
const formData = new FormData();
|
||||
formData.append('client_id', clientId);
|
||||
formData.append('client_secret', "{{ client_secret_value }}"); // Use the pre-configured secret
|
||||
formData.append('redirect_uri', redirectUri);
|
||||
formData.append('code', code);
|
||||
formData.append('tenant_id', tenantId || 'common');
|
||||
|
||||
// Show more details in processing message
|
||||
document.getElementById('processing-message').innerHTML =
|
||||
'<p>Exchanging authorization code for refresh token...</p>' +
|
||||
'<p class="text-xs text-gray-500 mt-2">Using tenant: ' + (tenantId || 'common') + '</p>';
|
||||
|
||||
fetch('/api/onedrive/exchange-token', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error(err.detail || 'Failed to exchange token');
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then(data => {
|
||||
if (data.refresh_token) {
|
||||
// Instead of saving to .env file, update settings in memory
|
||||
const updateFormData = new FormData();
|
||||
updateFormData.append('refresh_token', data.refresh_token);
|
||||
|
||||
// Use the existing clientId and tenantId if they were provided
|
||||
if (clientId) updateFormData.append('client_id', clientId);
|
||||
if (tenantId) updateFormData.append('tenant_id', tenantId);
|
||||
|
||||
document.getElementById('processing-message').innerHTML =
|
||||
'<p>Updating system settings with new token...</p>';
|
||||
|
||||
return fetch('/api/onedrive/update-settings', {
|
||||
method: 'POST',
|
||||
body: updateFormData
|
||||
}).then(response => {
|
||||
if (!response.ok) {
|
||||
return response.json().then(err => {
|
||||
throw new Error('Token received but failed to update settings: ' + (err.detail || 'Unknown error'));
|
||||
});
|
||||
}
|
||||
return response.json();
|
||||
}).then(() => {
|
||||
// Show the success message and environment variables
|
||||
showSuccess(data.refresh_token, clientId, tenantId);
|
||||
// In 10 seconds, redirect to status page (giving more time to copy)
|
||||
setTimeout(() => {
|
||||
window.location.href = '/status';
|
||||
}, 10000);
|
||||
});
|
||||
} else {
|
||||
throw new Error('No refresh token was received from the server');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
showError(error.message);
|
||||
});
|
||||
}
|
||||
|
||||
function showError(message) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('error-container').classList.remove('hidden');
|
||||
document.getElementById('error-message').innerText = message;
|
||||
}
|
||||
|
||||
function showSuccess(refreshToken, clientId, tenantId) {
|
||||
document.getElementById('processing-message').classList.add('hidden');
|
||||
document.getElementById('success-container').classList.remove('hidden');
|
||||
|
||||
// Update the environment variables pre block with the new token
|
||||
const envVarsCode = document.querySelector('#env-vars code');
|
||||
if (envVarsCode) {
|
||||
envVarsCode.textContent = `ONEDRIVE_CLIENT_ID=${clientId}
|
||||
ONEDRIVE_CLIENT_SECRET={{ client_secret_value }}
|
||||
ONEDRIVE_TENANT_ID=${tenantId || 'common'}
|
||||
ONEDRIVE_REFRESH_TOKEN=${refreshToken}
|
||||
ONEDRIVE_FOLDER_PATH=Documents/Uploads`;
|
||||
}
|
||||
|
||||
// Add copy functionality
|
||||
const copyEnvVarsBtn = document.getElementById('copy-env-vars');
|
||||
if (copyEnvVarsBtn) {
|
||||
copyEnvVarsBtn.addEventListener('click', function() {
|
||||
const envVarsText = document.getElementById('env-vars').textContent;
|
||||
navigator.clipboard.writeText(envVarsText)
|
||||
.then(() => {
|
||||
const originalText = copyEnvVarsBtn.textContent;
|
||||
copyEnvVarsBtn.textContent = 'Copied!';
|
||||
copyEnvVarsBtn.classList.add('bg-green-700');
|
||||
setTimeout(() => {
|
||||
copyEnvVarsBtn.textContent = originalText;
|
||||
copyEnvVarsBtn.classList.remove('bg-green-700');
|
||||
}, 2000);
|
||||
})
|
||||
.catch(err => {
|
||||
console.error('Failed to copy: ', err);
|
||||
alert('Failed to copy text to clipboard');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,40 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}OneDrive Authorization Error{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mx-auto px-4 py-8">
|
||||
<div class="bg-white shadow-md rounded-lg p-6">
|
||||
<div class="text-center mb-6">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="mx-auto h-12 w-12 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<h2 class="text-2xl font-bold mt-4">Authorization Failed</h2>
|
||||
<p class="text-gray-600 mt-2">Sorry, we couldn't complete the OneDrive authorization.</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<div class="bg-red-50 border-l-4 border-red-400 p-4">
|
||||
<div class="flex">
|
||||
<div class="flex-shrink-0">
|
||||
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ml-3">
|
||||
<h3 class="text-sm font-medium text-red-800">Error details:</h3>
|
||||
<div class="mt-2 text-sm text-red-700">
|
||||
<p>{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<a href="/onedrive-setup" class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
Return to Setup
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -43,9 +43,11 @@
|
||||
{% elif provider.icon == "google" %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
{% elif provider.icon == "microsoft" %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1V5zM4 15a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1H5a1 1 0 01-1-1v-4zm10 0a1 1 0 011-1h4a1 1 0 011 1v4a1 1 0 01-1 1h-4a1 1 0 01-1-1v-4z" />
|
||||
{% elif provider.icon == "activity" %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
{% elif provider.icon == "ftp" %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16l-6-6 6-6M2 10h16m4 0l-6 6m0-12l6 6" />
|
||||
{% else %}
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
{% endif %}
|
||||
|
||||
Reference in New Issue
Block a user