feat: Add manual OAuth authorization mode for WebUI (#131)
* feat: add manual OAuth flow support in WebUI * fix: reset add account modal state on close * feat: display custom API key in startup banner * fix: move translations to separate files and optimize import API * fix: remove orphaned model-manager.js and cleanup callback server on manual auth --------- Co-authored-by: Badri Narayanan S <59133612+badrisnarayanan@users.noreply.github.com>
This commit is contained in:
@@ -241,5 +241,90 @@ window.Components.accountManager = () => ({
|
||||
percent: Math.round(val * 100),
|
||||
model: bestModel
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Export accounts to JSON file
|
||||
*/
|
||||
async exportAccounts() {
|
||||
const store = Alpine.store('global');
|
||||
try {
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
'/api/accounts/export',
|
||||
{},
|
||||
store.webuiPassword
|
||||
);
|
||||
if (newPassword) store.webuiPassword = newPassword;
|
||||
|
||||
const data = await response.json();
|
||||
// API returns plain array directly
|
||||
if (Array.isArray(data)) {
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `antigravity-accounts-${new Date().toISOString().split('T')[0]}.json`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
store.showToast(store.t('exportSuccess', { count: data.length }), 'success');
|
||||
} else if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
} catch (e) {
|
||||
store.showToast(store.t('exportFailed') + ': ' + e.message, 'error');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Import accounts from JSON file
|
||||
* @param {Event} event - file input change event
|
||||
*/
|
||||
async importAccounts(event) {
|
||||
const store = Alpine.store('global');
|
||||
const file = event.target.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
try {
|
||||
const text = await file.text();
|
||||
const importData = JSON.parse(text);
|
||||
|
||||
// Support both plain array and wrapped format
|
||||
const accounts = Array.isArray(importData) ? importData : (importData.accounts || []);
|
||||
if (!Array.isArray(accounts) || accounts.length === 0) {
|
||||
throw new Error('Invalid file format: expected accounts array');
|
||||
}
|
||||
|
||||
const { response, newPassword } = await window.utils.request(
|
||||
'/api/accounts/import',
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(accounts)
|
||||
},
|
||||
store.webuiPassword
|
||||
);
|
||||
if (newPassword) store.webuiPassword = newPassword;
|
||||
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
const { added, updated, failed } = data.results;
|
||||
let msg = store.t('importSuccess') + ` ${added.length} added, ${updated.length} updated`;
|
||||
if (failed.length > 0) {
|
||||
msg += `, ${failed.length} failed`;
|
||||
}
|
||||
store.showToast(msg, failed.length > 0 ? 'info' : 'success');
|
||||
Alpine.store('data').fetchData();
|
||||
} else {
|
||||
throw new Error(data.error || 'Import failed');
|
||||
}
|
||||
} catch (e) {
|
||||
store.showToast(store.t('importFailed') + ': ' + e.message, 'error');
|
||||
} finally {
|
||||
// Reset file input
|
||||
event.target.value = '';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
88
public/js/components/add-account-modal.js
Normal file
88
public/js/components/add-account-modal.js
Normal file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* Add Account Modal Component
|
||||
* Registers itself to window.Components for Alpine.js to consume
|
||||
*/
|
||||
window.Components = window.Components || {};
|
||||
|
||||
window.Components.addAccountModal = () => ({
|
||||
manualMode: false,
|
||||
authUrl: '',
|
||||
authState: '',
|
||||
callbackInput: '',
|
||||
submitting: false,
|
||||
|
||||
/**
|
||||
* Reset all state to initial values
|
||||
*/
|
||||
resetState() {
|
||||
this.manualMode = false;
|
||||
this.authUrl = '';
|
||||
this.authState = '';
|
||||
this.callbackInput = '';
|
||||
this.submitting = false;
|
||||
// Close any open details elements
|
||||
const details = document.querySelectorAll('#add_account_modal details[open]');
|
||||
details.forEach(d => d.removeAttribute('open'));
|
||||
},
|
||||
|
||||
async copyLink() {
|
||||
if (!this.authUrl) return;
|
||||
await navigator.clipboard.writeText(this.authUrl);
|
||||
Alpine.store('global').showToast(Alpine.store('global').t('linkCopied'), 'success');
|
||||
},
|
||||
|
||||
async initManualAuth(event) {
|
||||
if (event.target.open && !this.authUrl) {
|
||||
try {
|
||||
const password = Alpine.store('global').webuiPassword;
|
||||
const {
|
||||
response,
|
||||
newPassword
|
||||
} = await window.utils.request('/api/auth/url', {}, password);
|
||||
if (newPassword) Alpine.store('global').webuiPassword = newPassword;
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
this.authUrl = data.url;
|
||||
this.authState = data.state;
|
||||
}
|
||||
} catch (e) {
|
||||
Alpine.store('global').showToast(e.message, 'error');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
async completeManualAuth() {
|
||||
if (!this.callbackInput || !this.authState) return;
|
||||
this.submitting = true;
|
||||
try {
|
||||
const store = Alpine.store('global');
|
||||
const {
|
||||
response,
|
||||
newPassword
|
||||
} = await window.utils.request('/api/auth/complete', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
callbackInput: this.callbackInput,
|
||||
state: this.authState
|
||||
})
|
||||
}, store.webuiPassword);
|
||||
if (newPassword) store.webuiPassword = newPassword;
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
store.showToast(store.t('accountAddedSuccess'), 'success');
|
||||
Alpine.store('data').fetchData();
|
||||
document.getElementById('add_account_modal').close();
|
||||
this.resetState();
|
||||
} else {
|
||||
store.showToast(data.error || store.t('authFailed'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
Alpine.store('global').showToast(e.message, 'error');
|
||||
} finally {
|
||||
this.submitting = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user