You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
OneAuth/ui/page/sys/oauth/index.html

242 lines
6.0 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta name="description" content="OAuth Apps">
<title>{{ $t('nav.oauth') }}</title>
<style>
body {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
padding: var(--spacing-lg);
box-sizing: border-box;
background-color: var(--bg-color);
}
.page-header {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: var(--spacing-md);
padding-bottom: var(--spacing-md);
border-bottom: 1px solid var(--border-color);
}
.page-title {
font-size: var(--font-size-2xl);
font-weight: var(--font-weight-bold);
color: var(--text-color);
display: flex;
align-items: center;
gap: var(--spacing-sm);
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: var(--spacing-lg);
overflow-y: auto;
padding: 4px;
}
.app-card {
background: var(--bg-color-secondary);
padding: var(--spacing-lg);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
border: 1px solid var(--border-color);
transition: all var(--transition-base);
}
.app-card:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
.app-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.app-name {
font-size: var(--font-size-lg);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
}
.info-row {
display: flex;
flex-direction: column;
gap: 4px;
}
.info-label {
font-size: var(--font-size-xs);
color: var(--text-color-tertiary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.info-value {
font-size: var(--font-size-sm);
color: var(--text-color-secondary);
font-family: monospace;
background: var(--bg-color-tertiary);
padding: 6px;
border-radius: var(--radius-sm);
word-break: break-all;
}
.actions {
display: flex;
justify-content: flex-end;
gap: var(--spacing-sm);
margin-top: auto;
padding-top: var(--spacing-md);
border-top: 1px solid var(--border-color);
}
</style>
</head>
<body>
<div class="page-header">
<div class="page-title">
<i class="fas fa-key" style="color: var(--color-primary);"></i>
{{ $t('nav.oauth') }}
</div>
<v-btn color="primary" :click="openCreateModal">
<i class="fas fa-plus"></i>
{{ $t('oauth.create_app') }}
</v-btn>
</div>
<div class="app-grid">
<div class="app-card" v-for="app in apps">
<div class="app-header">
<div class="app-name">{{ app.name }}</div>
<div class="info-value" style="font-size: 10px; background: transparent; padding: 0;">{{ app.id }}</div>
</div>
<div class="info-row">
<span class="info-label">Client ID</span>
<div class="info-value">{{ app.client_id }}</div>
</div>
<div class="info-row">
<span class="info-label">Redirect URI</span>
<div class="info-value">{{ app.redirect_uri }}</div>
</div>
<div class="actions">
<v-btn icon size="sm" variant="outline" :click="() => openEditModal(app)" title="Edit">
<i class="fas fa-edit"></i>
</v-btn>
<v-btn icon size="sm" color="danger" variant="outline" :click="() => deleteApp(app)" title="Delete">
<i class="fas fa-trash"></i>
</v-btn>
</div>
</div>
</div>
<!-- Create/Edit Dialog -->
<v-dialog v:visible="showModal"
:title="isEdit ? $t('oauth.edit') : $t('oauth.create')">
<form @submit.prevent="saveApp" style="display: grid; gap: 16px;">
<v-input label="App Name" required v:value="formData.name" placeholder="e.g. My Awesome App"></v-input>
<v-input label="Redirect URI" required v:value="formData.redirect_uri"
placeholder="e.g. http://localhost:3000/callback"></v-input>
</form>
<div vslot="footer">
<v-btn variant="outline" :click="closeModal">{{ $t('common.cancel') }}</v-btn>
<v-btn color="primary" :click="saveApp">
{{ isEdit ? $t('common.save') : $t('common.create') }}
</v-btn>
</div>
</v-dialog>
</body>
<script setup>
apps = [];
showModal = false;
isEdit = false;
formData = {
id: null,
name: "",
redirect_uri: ""
};
loadApps = async () => {
try {
const res = await $axios.get('/api/oauth/clients');
apps = res.items || [];
} catch (e) {
$message.error(e.message);
}
};
openCreateModal = () => {
isEdit = false;
formData = { id: null, name: "", redirect_uri: "http://localhost:3000/callback" };
showModal = true;
};
openEditModal = (app) => {
isEdit = true;
formData = { ...app };
showModal = true;
};
closeModal = () => {
showModal = false;
};
saveApp = async () => {
if (!formData.name || !formData.redirect_uri) {
$message.error("Name and Redirect URI are required");
return;
}
try {
if (isEdit) {
await $axios.patch(`/api/oauth/clients/${formData.id}`, {
name: formData.name,
redirect_uri: formData.redirect_uri
});
$message.success("App updated");
} else {
await $axios.post('/api/oauth/clients', {
name: formData.name,
redirect_uri: formData.redirect_uri
});
$message.success("App created");
}
closeModal();
loadApps();
} catch (e) {
$message.error(e.message);
}
};
deleteApp = async (app) => {
try {
await $message.confirm(`Delete app "${app.name}"?`);
await $axios.delete(`/api/oauth/clients/${app.id}`);
$message.success("Deleted");
loadApps();
} catch (e) {
// Cancelled
}
};
</script>
<script>
$data.loadApps();
</script>
</html>