Add Google Drive and Photos support with headless auth for storage providers

This commit is contained in:
StarFleetCPTN
2025-04-18 12:41:10 -07:00
parent b819162732
commit 23db71c4a8
10 changed files with 1341 additions and 19 deletions
+169
View File
@@ -0,0 +1,169 @@
package components
import (
"context"
)
// GDriveHeadlessAuthData contains data needed for rendering the headless auth page
type GDriveHeadlessAuthData struct {
AuthCommand string
ConfigID string
}
// GDriveHeadlessAuth renders the headless authentication page for Google Drive/Photos
templ GDriveHeadlessAuth(ctx context.Context, data GDriveHeadlessAuthData) {
// Force the layout to display as authenticated content
@LayoutWithContext("Google Authentication - Headless Mode", ctx) {
<style>
/* Ensure proper styling for the headless auth page */
body.dark .auth-page {
background-color: #111827 !important;
}
</style>
<div id="auth-container" class="auth-page w-full pb-8 bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; background-color: rgb(249, 250, 251);">
<div class="max-w-4xl mx-auto">
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-key w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
Headless Google Authentication
</h1>
<a href="/configs" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
Back to Configurations
</a>
</div>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
<div class="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0 mt-0.5">
<i class="fas fa-info-circle h-5 w-5 text-blue-500"></i>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700 dark:text-blue-300">
You need to authenticate with Google using a web browser. Since you're running GoMFT behind a reverse proxy or in a headless environment, you'll need to complete authentication on a machine with a web browser.
</p>
</div>
</div>
</div>
<div class="mb-8">
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 1: Run the following command on a machine with a web browser</h2>
<div class="relative mb-4">
<pre id="auth-command-text" class="bg-gray-50 dark:bg-gray-900 rounded-md p-4 overflow-x-auto text-sm font-mono">{ data.AuthCommand }</pre>
<button id="copy-command" class="absolute top-2 right-2 bg-gray-200 dark:bg-gray-700 p-1.5 rounded hover:bg-gray-300 dark:hover:bg-gray-600" title="Copy to clipboard">
<i class="fas fa-copy h-5 w-5 text-gray-700 dark:text-gray-300"></i>
</button>
</div>
<div>
<h3 class="text-md font-medium mb-2 text-gray-900 dark:text-white">What this command does:</h3>
<ul class="list-disc ml-6 text-sm text-gray-700 dark:text-gray-300 space-y-1">
<li>Opens a browser window on the machine where you run it</li>
<li>Allows you to authenticate with Google</li>
<li>Generates an authentication token</li>
</ul>
</div>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 2: Paste the authentication token below</h2>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-4">
After completing authentication in the browser, you'll receive a token. Copy and paste that token here:
</p>
<form action="/configs/gdrive-headless-token" method="POST" class="space-y-4">
<input type="hidden" name="config_id" value={ data.ConfigID } />
<div>
<label for="auth_token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Authentication Token</label>
<textarea
id="auth_token"
name="auth_token"
rows="5"
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
placeholder="Paste your authentication token here..."
required
></textarea>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">The token will look like a long JSON string containing access credentials.</p>
</div>
<div class="flex items-center justify-end mt-6">
<a href="/configs" class="mr-4 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-400">
Cancel
</a>
<button
type="submit"
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"
>
Submit Token
</button>
</div>
</form>
</div>
</div>
<!-- Help Section -->
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-start">
<div class="flex items-center h-5">
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
</div>
<div class="ml-2 text-sm">
<p class="text-gray-700 dark:text-gray-300">This authentication process is necessary for GoMFT to access your Google Drive or Google Photos account.</p>
<p class="mt-1 text-gray-600 dark:text-gray-400">The token is only used for authentication and is stored securely. You'll only need to complete this process once for each configuration.</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Set dark background color if in dark mode
document.addEventListener('DOMContentLoaded', function() {
if (document.documentElement.classList.contains('dark')) {
document.getElementById('auth-container').style.backgroundColor = '#111827';
}
// Add event listener for theme changes
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
setTimeout(function() {
const isDark = document.documentElement.classList.contains('dark');
document.getElementById('auth-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
}, 50);
});
}
// Store the actual command text
const actualCommand = document.getElementById('auth-command-text').textContent.trim();
// Add click handler to copy button
document.getElementById('copy-command').addEventListener('click', function() {
// Copy the actual command text, not the template variable
navigator.clipboard.writeText(actualCommand).then(function() {
// Show a success message
const button = document.getElementById('copy-command');
const originalTitle = button.getAttribute('title');
button.setAttribute('title', 'Copied!');
// Also show visual feedback
button.classList.add('bg-green-200', 'dark:bg-green-700');
button.classList.remove('bg-gray-200', 'dark:bg-gray-700');
setTimeout(function() {
button.setAttribute('title', originalTitle);
button.classList.remove('bg-green-200', 'dark:bg-green-700');
button.classList.add('bg-gray-200', 'dark:bg-gray-700');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy text: ', err);
alert('Failed to copy command. Please select and copy it manually.');
});
});
});
</script>
}
}
+8 -8
View File
@@ -161,12 +161,12 @@ templ formFields(data StorageProviderFormData) {
selected="selected"
}
>OneDrive</option>
<option value="google_drive"
<option value="drive"
if data.Provider.Type == db.ProviderTypeGoogleDrive {
selected="selected"
}
>Google Drive</option>
<option value="google_photo"
<option value="gphotos"
if data.Provider.Type == db.ProviderTypeGooglePhoto {
selected="selected"
}
@@ -512,7 +512,7 @@ templ providerFormScript() {
}
// If Google Drive or Google Photos, update hidden fields
if (['google_drive', 'google_photo', 'onedrive'].includes(providerType)) {
if (['drive', 'gphotos', 'onedrive'].includes(providerType)) {
document.getElementById('hidden_clientID').value = document.getElementById('clientID').value;
// Make sure clientSecret is always copied to the hidden field
@@ -866,7 +866,7 @@ templ providerFormScript() {
}
// Google services
if (['google_drive', 'google_photo'].includes(provider)) {
if (['drive', 'gphotos'].includes(provider)) {
document.getElementById('cloud-fields').classList.remove('hidden');
document.getElementById('built-in-auth-field').classList.remove('hidden');
@@ -880,9 +880,9 @@ templ providerFormScript() {
if (editMode) {
// Edit mode - provider specific text
if (provider === 'google_drive') {
if (provider === 'drive') {
clientSecretDescription.textContent = "Leave empty to keep the current Google Drive client secret";
} else if (provider === 'google_photo') {
} else if (provider === 'gphotos') {
clientSecretDescription.textContent = "Leave empty to keep the current Google Photos client secret";
}
}
@@ -890,13 +890,13 @@ templ providerFormScript() {
}
// Google Drive specific fields
if (provider === 'google_drive') {
if (provider === 'drive') {
document.getElementById('drive-id-field').classList.remove('hidden');
document.getElementById('team-drive-field').classList.remove('hidden');
}
// Google Photos specific fields
if (provider === 'google_photo') {
if (provider === 'gphotos') {
document.getElementById('gphotos-options-field').classList.remove('hidden');
}
}
@@ -0,0 +1,169 @@
package components
import (
"context"
)
// StorageProviderGDriveHeadlessAuthData contains data needed for rendering the headless auth page for storage providers
type StorageProviderGDriveHeadlessAuthData struct {
AuthCommand string
ProviderID string
}
// StorageProviderGDriveHeadlessAuth renders the headless authentication page for Google Drive/Photos for storage providers
templ StorageProviderGDriveHeadlessAuth(ctx context.Context, data StorageProviderGDriveHeadlessAuthData) {
// Force the layout to display as authenticated content
@LayoutWithContext("Google Authentication - Headless Mode", ctx) {
<style>
/* Ensure proper styling for the headless auth page */
body.dark .auth-page {
background-color: #111827 !important;
}
</style>
<div id="auth-container" class="auth-page w-full pb-8 bg-gray-50 dark:bg-gray-900" style="min-height: 100vh; background-color: rgb(249, 250, 251);">
<div class="max-w-4xl mx-auto">
<div class="mb-6 flex flex-col md:flex-row md:items-center md:justify-between gap-4">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white flex items-center">
<i class="fas fa-key w-6 h-6 mr-2 text-blue-500 dark:text-blue-400"></i>
Headless Google Authentication
</h1>
<a href="/storage-providers" class="flex items-center justify-center text-gray-700 bg-gray-100 hover:bg-gray-200 focus:ring-4 focus:ring-gray-300 font-medium rounded-lg px-5 py-2.5 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 focus:outline-none dark:focus:ring-gray-700">
<i class="fas fa-arrow-left w-4 h-4 mr-2"></i>
Back to Storage Providers
</a>
</div>
<div class="bg-white border border-gray-200 rounded-lg shadow-sm dark:border-gray-700 dark:bg-gray-800 p-6">
<div class="bg-blue-50 dark:bg-blue-900/30 border-l-4 border-blue-500 p-4 mb-6">
<div class="flex">
<div class="flex-shrink-0 mt-0.5">
<i class="fas fa-info-circle h-5 w-5 text-blue-500"></i>
</div>
<div class="ml-3">
<p class="text-sm text-blue-700 dark:text-blue-300">
You need to authenticate with Google using a web browser. Since you're running GoMFT behind a reverse proxy or in a headless environment, you'll need to complete authentication on a machine with a web browser.
</p>
</div>
</div>
</div>
<div class="mb-8">
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 1: Run the following command on a machine with a web browser</h2>
<div class="relative mb-4">
<pre id="auth-command-text" class="bg-gray-50 dark:bg-gray-900 rounded-md p-4 overflow-x-auto text-sm font-mono">{ data.AuthCommand }</pre>
<button id="copy-command" class="absolute top-2 right-2 bg-gray-200 dark:bg-gray-700 p-1.5 rounded hover:bg-gray-300 dark:hover:bg-gray-600" title="Copy to clipboard">
<i class="fas fa-copy h-5 w-5 text-gray-700 dark:text-gray-300"></i>
</button>
</div>
<div>
<h3 class="text-md font-medium mb-2 text-gray-900 dark:text-white">What this command does:</h3>
<ul class="list-disc ml-6 text-sm text-gray-700 dark:text-gray-300 space-y-1">
<li>Opens a browser window on the machine where you run it</li>
<li>Allows you to authenticate with Google</li>
<li>Generates an authentication token</li>
</ul>
</div>
</div>
<div class="mb-6">
<h2 class="text-lg font-medium mb-3 text-gray-900 dark:text-white">Step 2: Paste the authentication token below</h2>
<p class="text-sm text-gray-700 dark:text-gray-300 mb-4">
After completing authentication in the browser, you'll receive a token. Copy and paste that token here:
</p>
<form action="/storage-providers/gdrive-headless-token" method="POST" class="space-y-4">
<input type="hidden" name="provider_id" value={ data.ProviderID } />
<div>
<label for="auth_token" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Authentication Token</label>
<textarea
id="auth_token"
name="auth_token"
rows="5"
class="mt-1 block w-full rounded-md border border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:placeholder-gray-400"
placeholder="Paste your authentication token here..."
required
></textarea>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">The token will look like a long JSON string containing access credentials.</p>
</div>
<div class="flex items-center justify-end mt-6">
<a href="/storage-providers" class="mr-4 px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500 dark:hover:text-gray-400">
Cancel
</a>
<button
type="submit"
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"
>
Submit Token
</button>
</div>
</form>
</div>
</div>
<!-- Help Section -->
<div class="bg-gray-50 dark:bg-gray-800 rounded-lg shadow-sm mt-8 p-4 border border-gray-200 dark:border-gray-700">
<div class="flex items-start">
<div class="flex items-center h-5">
<i class="fas fa-info-circle w-4 h-4 text-blue-500 dark:text-blue-400 mr-2"></i>
</div>
<div class="ml-2 text-sm">
<p class="text-gray-700 dark:text-gray-300">This authentication process is necessary for GoMFT to access your Google Drive or Google Photos account.</p>
<p class="mt-1 text-gray-600 dark:text-gray-400">The token is only used for authentication and is stored securely. You'll only need to complete this process once for each storage provider.</p>
</div>
</div>
</div>
</div>
</div>
<script>
// Set dark background color if in dark mode
document.addEventListener('DOMContentLoaded', function() {
if (document.documentElement.classList.contains('dark')) {
document.getElementById('auth-container').style.backgroundColor = '#111827';
}
// Add event listener for theme changes
const themeToggle = document.getElementById('theme-toggle');
if (themeToggle) {
themeToggle.addEventListener('click', function() {
setTimeout(function() {
const isDark = document.documentElement.classList.contains('dark');
document.getElementById('auth-container').style.backgroundColor = isDark ? '#111827' : 'rgb(249, 250, 251)';
}, 50);
});
}
// Store the actual command text
const actualCommand = document.getElementById('auth-command-text').textContent.trim();
// Add click handler to copy button
document.getElementById('copy-command').addEventListener('click', function() {
// Copy the actual command text, not the template variable
navigator.clipboard.writeText(actualCommand).then(function() {
// Show a success message
const button = document.getElementById('copy-command');
const originalTitle = button.getAttribute('title');
button.setAttribute('title', 'Copied!');
// Also show visual feedback
button.classList.add('bg-green-200', 'dark:bg-green-700');
button.classList.remove('bg-gray-200', 'dark:bg-gray-700');
setTimeout(function() {
button.setAttribute('title', originalTitle);
button.classList.remove('bg-green-200', 'dark:bg-green-700');
button.classList.add('bg-gray-200', 'dark:bg-gray-700');
}, 2000);
}).catch(function(err) {
console.error('Failed to copy text: ', err);
alert('Failed to copy command. Please select and copy it manually.');
});
});
});
</script>
}
}
+139 -5
View File
@@ -306,6 +306,7 @@ templ StorageProviders(ctx context.Context, data StorageProvidersData) {
</div>
<div class="ml-2 text-sm">
<p class="text-gray-700 dark:text-gray-300">Your credentials are encrypted for security. You can test connections before using them in transfers.</p>
<p class="mt-1 text-gray-600 dark:text-gray-400">Google Drive and Google Photos providers require authentication. Click the "Authenticate" button to complete setup.</p>
</div>
</div>
@@ -322,6 +323,73 @@ templ StorageProviders(ctx context.Context, data StorageProvidersData) {
</div>
<script src="/static/js/storage-providers.js"></script>
<script>
// Function to initialize all auth dropdowns
function initAllAuthDropdowns() {
// Get all dropdown buttons
const dropdownButtons = document.querySelectorAll('[id^="auth-dropdown-button-"]');
dropdownButtons.forEach(button => {
const providerId = button.getAttribute('data-provider-id');
const menu = document.getElementById(`auth-dropdown-menu-${providerId}`);
if (button && menu) {
// Add click listener
button.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Position dropdown based on available space
const buttonRect = button.getBoundingClientRect();
const spaceRight = window.innerWidth - buttonRect.right;
const spaceLeft = buttonRect.left;
// Check if there's more space on the left or right side
if (spaceLeft > spaceRight) {
menu.classList.add('right-0');
menu.classList.remove('left-0');
} else {
menu.classList.add('left-0');
menu.classList.remove('right-0');
}
// Toggle visibility
menu.classList.toggle('hidden');
console.log(`Toggled dropdown for provider ${providerId}`);
});
console.log(`Initialized dropdown for provider ${providerId}`);
} else {
console.error(`Could not find dropdown elements for provider ${providerId}`);
}
});
// Close dropdowns when clicking elsewhere
document.addEventListener('click', function(e) {
dropdownButtons.forEach(button => {
const providerId = button.getAttribute('data-provider-id');
const menu = document.getElementById(`auth-dropdown-menu-${providerId}`);
if (menu && !button.contains(e.target) && !menu.contains(e.target)) {
menu.classList.add('hidden');
}
});
});
}
// Initialize dropdowns when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
// Check for status messages based on URL parameters
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('status') === 'gdrive_auth_success') {
showToast("Google Drive authentication completed successfully", 'success');
}
// Initialize all authentication dropdowns
initAllAuthDropdowns();
});
</script>
}
}
@@ -416,11 +484,11 @@ templ StorageProviders_ProvidersList(providers []db.StorageProvider) {
Region: { provider.Region }
</p>
}
} else if provider.Type == "google_drive" || provider.Type == "google_photo" {
} else if provider.Type == "drive" || provider.Type == "gphotos" {
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
<i class="fab fa-google-drive w-4 h-4 mr-1.5 text-gray-400 dark:text-gray-500"></i>
Google
if provider.Type == "google_drive" {
if provider.Type == "drive" {
Drive
} else {
Photos
@@ -431,12 +499,51 @@ templ StorageProviders_ProvidersList(providers []db.StorageProvider) {
</p>
if provider.Authenticated != nil && *provider.Authenticated {
<span class="mt-2 md:mt-0 bg-green-100 text-green-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300">
<i class="fas fa-check-circle w-3 h-3 mr-1 inline"></i>
Authenticated
</span>
} else {
<span class="mt-2 md:mt-0 bg-yellow-100 text-yellow-800 text-xs font-medium mr-2 px-2.5 py-0.5 rounded-full dark:bg-yellow-900 dark:text-yellow-300">
Not Authenticated
</span>
<div class="flex flex-col md:flex-row items-start md:items-center mt-2 md:mt-0">
<!-- Google Authentication Dropdown -->
<div class="relative inline-block text-left mt-2 md:mt-0">
<button
id={ fmt.Sprintf("auth-dropdown-button-%d", provider.ID) }
data-provider-id={ fmt.Sprintf("%d", provider.ID) }
type="button"
class="text-yellow-700 bg-yellow-100 hover:bg-yellow-200 focus:ring-4 focus:outline-none focus:ring-yellow-300 font-medium rounded-lg text-sm px-3 py-1.5 text-center inline-flex items-center dark:bg-yellow-900 dark:text-yellow-300 dark:hover:bg-yellow-800 dark:focus:ring-yellow-800"
aria-expanded="false"
aria-haspopup="true">
<i class="fas fa-key w-3.5 h-3.5 mr-1.5"></i>
Authenticate with Google
<i class="fas fa-chevron-down w-3.5 h-3.5 ml-1.5"></i>
</button>
<div id={ fmt.Sprintf("auth-dropdown-menu-%d", provider.ID) } class="origin-top-right absolute left-0 mt-2 w-56 rounded-md shadow-lg bg-white dark:bg-gray-700 ring-1 ring-black ring-opacity-5 focus:outline-none z-50 hidden" style="max-height: 200px; overflow-y: auto;" role="menu" aria-orientation="vertical" aria-labelledby={ fmt.Sprintf("auth-dropdown-button-%d", provider.ID) }>
<div class="py-1" role="none">
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 group flex items-center px-4 py-2 text-sm" role="menuitem">
<i class="fas fa-globe w-4 h-4 mr-3 text-gray-500 dark:text-gray-400"></i>
Standard Authentication
</a>
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-600 group flex items-center px-4 py-2 text-sm" role="menuitem">
<i class="fas fa-terminal w-4 h-4 mr-3 text-gray-500 dark:text-gray-400"></i>
Headless Authentication
</a>
</div>
</div>
</div>
<!-- Hidden fallback links - only shown when JavaScript is disabled -->
<noscript>
<div class="flex flex-col text-xs text-gray-500 dark:text-gray-400 mt-1 ml-1">
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="hover:underline hover:text-blue-500">
Direct Standard Auth
</a>
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="hover:underline hover:text-blue-500">
Direct Headless Auth
</a>
</div>
</noscript>
</div>
}
} else if provider.Type == "webdav" || provider.Type == "nextcloud" {
<p class="flex items-center text-sm text-gray-500 dark:text-gray-400">
@@ -493,6 +600,33 @@ templ StorageProviders_ProvidersList(providers []db.StorageProvider) {
</div>
</div>
</div>
<!-- Authentication Notice (More Visible) -->
if (provider.Type == "drive" || provider.Type == "gphotos") && (provider.Authenticated == nil || !*provider.Authenticated) {
<div class="mt-3 flex items-center justify-between bg-yellow-50 dark:bg-yellow-900/30 rounded-lg p-3 border border-yellow-200 dark:border-yellow-800">
<div class="flex items-center">
<i class="fas fa-exclamation-triangle text-yellow-500 w-5 h-5 mr-2"></i>
<span class="text-sm text-yellow-700 dark:text-yellow-300">
Authentication required for Google
if provider.Type == "drive" {
Drive
} else {
Photos
}
</span>
</div>
<div class="flex gap-2">
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-auth", provider.ID)) } class="text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 px-3 py-1.5 rounded-lg">
<i class="fas fa-globe w-3.5 h-3.5 mr-1.5"></i>
Standard Auth
</a>
<a href={ templ.SafeURL(fmt.Sprintf("/storage-providers/%d/gdrive-headless-auth", provider.ID)) } class="text-sm font-medium text-white bg-green-600 hover:bg-green-700 px-3 py-1.5 rounded-lg">
<i class="fas fa-terminal w-3.5 h-3.5 mr-1.5"></i>
Headless Auth
</a>
</div>
</div>
}
</div>
</li>
}