first commit
This commit is contained in:
631
frontend/src/pages/Admin.vue
Normal file
631
frontend/src/pages/Admin.vue
Normal file
@@ -0,0 +1,631 @@
|
||||
<template>
|
||||
<div class="admin-page">
|
||||
<div class="d-flex justify-content-between align-items-start mb-3 flex-wrap gap-2">
|
||||
<div>
|
||||
<h2 class="mb-1">Admin Panel</h2>
|
||||
<p class="text-muted mb-0">Manage users, groups, spaces, and identity providers.</p>
|
||||
</div>
|
||||
<button class="btn btn-outline-secondary" @click="router.push('/')">Back to Notes</button>
|
||||
</div>
|
||||
|
||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
|
||||
|
||||
<ul class="nav nav-tabs mb-3">
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'users' }" @click="activeTab = 'users'">Users</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'groups' }" @click="activeTab = 'groups'">Groups</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'spaces' }" @click="activeTab = 'spaces'">Spaces</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'providers' }" @click="activeTab = 'providers'">Identity Providers</button>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<button class="nav-link" :class="{ active: activeTab === 'featureFlags' }" @click="activeTab = 'featureFlags'">Feature Flags</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<section v-if="activeTab === 'users'" class="admin-section card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">All Users</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loadingUsers" @click="loadUsers">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingUsers" class="text-muted small">Loading users...</div>
|
||||
<div v-else-if="users.length === 0" class="border rounded p-3 text-muted">No users found.</div>
|
||||
<div v-else class="table-responsive">
|
||||
<table class="table table-sm table-hover align-middle mb-0">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Email</th>
|
||||
<th>Groups</th>
|
||||
<th>Status</th>
|
||||
<th>Joined</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="u in users" :key="u.id">
|
||||
<td>{{ u.username }}</td>
|
||||
<td class="text-muted small">{{ u.email }}</td>
|
||||
<td style="min-width: 260px">
|
||||
<select
|
||||
class="form-select form-select-sm"
|
||||
multiple
|
||||
:value="u.group_ids || []"
|
||||
@change="
|
||||
updateUserGroups(
|
||||
u.id,
|
||||
Array.from($event.target.selectedOptions).map((option) => option.value),
|
||||
)
|
||||
"
|
||||
>
|
||||
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
<div class="small text-muted mt-1">Ctrl/Cmd+Click for multiple groups</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="badge" :class="u.is_active ? 'text-bg-success' : 'text-bg-secondary'">
|
||||
{{ u.is_active ? "Active" : "Inactive" }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-muted small">{{ formatDate(u.created_at) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'groups'" class="admin-section card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Permission Groups</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loadingGroups" @click="loadGroups">Refresh</button>
|
||||
<button class="btn btn-sm btn-primary" @click="openCreateGroupModal">Create Group</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingGroups" class="text-muted small">Loading groups...</div>
|
||||
<div v-else-if="groups.length === 0" class="border rounded p-3 text-muted">No groups created yet.</div>
|
||||
<div v-else class="list-group">
|
||||
<div v-for="group in groups" :key="group.id" class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold d-flex align-items-center gap-2">
|
||||
<span>{{ group.name }}</span>
|
||||
<span v-if="group.is_system" class="badge text-bg-dark">System</span>
|
||||
</div>
|
||||
<div class="small text-muted">{{ group.description || "No description" }}</div>
|
||||
<div class="small text-muted">{{ (group.permissions || []).length }} permission{{ (group.permissions || []).length === 1 ? "" : "s" }}</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-outline-primary" @click="openEditGroupModal(group)">Edit</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'spaces'" class="admin-section card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">All Spaces</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loadingSpaces" @click="loadSpaces">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingSpaces" class="text-muted small">Loading spaces...</div>
|
||||
<div v-else-if="spaces.length === 0" class="border rounded p-3 text-muted">No spaces found.</div>
|
||||
<div v-else class="list-group mb-3">
|
||||
<div v-for="space in spaces" :key="space.id" class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ space.name }}</div>
|
||||
<div class="small text-muted">{{ space.description || "No description" }}</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge" :class="space.is_public ? 'text-bg-success' : 'text-bg-secondary'">
|
||||
{{ space.is_public ? "Public" : "Private" }}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-outline-primary" @click="openSpaceModal(space)">Edit Space</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'providers'" class="admin-section card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h5 class="mb-0">Configured Providers</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loadingProviders" @click="loadProviders">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingProviders" class="text-muted small">Loading providers...</div>
|
||||
<div v-else-if="providers.length === 0" class="border rounded p-3 text-muted">No providers configured yet.</div>
|
||||
<div v-else class="list-group mb-3">
|
||||
<div v-for="provider in providers" :key="provider.id" class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">{{ provider.name }}</div>
|
||||
<div class="small text-muted">{{ provider.type.toUpperCase() }} · {{ provider.scopes.join(", ") }}</div>
|
||||
<div class="small text-muted">Callback: {{ buildCallbackUrl(provider.id) }}</div>
|
||||
</div>
|
||||
<span class="badge" :class="provider.is_active ? 'text-bg-success' : 'text-bg-secondary'">
|
||||
{{ provider.is_active ? "Active" : "Disabled" }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h6 class="mb-2">Add Provider</h6>
|
||||
<form class="row g-3" @submit.prevent="createProvider">
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Display Name</label>
|
||||
<input v-model="providerForm.name" type="text" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Provider Type</label>
|
||||
<select v-model="providerForm.type" class="form-select">
|
||||
<option value="oidc">OIDC</option>
|
||||
<option value="oauth2">OAuth2</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client ID</label>
|
||||
<input v-model="providerForm.client_id" type="text" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client Secret</label>
|
||||
<input v-model="providerForm.client_secret" type="password" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Authorization URL</label>
|
||||
<input v-model="providerForm.authorization_url" type="url" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Token URL</label>
|
||||
<input v-model="providerForm.token_url" type="url" class="form-control" required />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">UserInfo URL</label>
|
||||
<input v-model="providerForm.userinfo_url" type="url" class="form-control" placeholder="Optional" />
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID Token Field</label>
|
||||
<input v-model="providerForm.id_token_claim" type="text" class="form-control" placeholder="id_token" />
|
||||
</div>
|
||||
<div class="col-12">
|
||||
<label class="form-label">Scopes</label>
|
||||
<input v-model="providerForm.scopes" type="text" class="form-control" placeholder="openid, profile, email" />
|
||||
</div>
|
||||
<div class="col-12 form-check ms-2">
|
||||
<input id="provider-active" v-model="providerForm.is_active" type="checkbox" class="form-check-input" />
|
||||
<label for="provider-active" class="form-check-label">Provider is active</label>
|
||||
</div>
|
||||
<div class="col-12 d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-primary" :disabled="submittingProvider">
|
||||
{{ submittingProvider ? "Saving..." : "Add Provider" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="activeTab === 'featureFlags'" class="admin-section card border-0 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<h5 class="mb-0">Application Feature Flags</h5>
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loadingFeatureFlags" @click="loadFeatureFlags">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loadingFeatureFlags" class="text-muted small">Loading feature flags...</div>
|
||||
|
||||
<div v-else class="d-grid gap-3">
|
||||
<div class="feature-flag-item border rounded p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">Enable User Registration</div>
|
||||
<div class="small text-muted">Controls whether new users can sign up from the register page.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input id="flag-registration" v-model="featureFlagsForm.registration_enabled" class="form-check-input" type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-flag-item border rounded p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">Enable Provider Login</div>
|
||||
<div class="small text-muted">Controls OAuth/OIDC sign-in buttons and provider login endpoints.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input id="flag-provider-login" v-model="featureFlagsForm.provider_login_enabled" class="form-check-input" type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="feature-flag-item border rounded p-3 d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<div class="fw-semibold">Enable Public Sharing</div>
|
||||
<div class="small text-muted">Reserved for public content controls and future sharing gates.</div>
|
||||
</div>
|
||||
<div class="form-check form-switch m-0">
|
||||
<input id="flag-public-sharing" v-model="featureFlagsForm.public_sharing_enabled" class="form-check-input" type="checkbox" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex justify-content-end">
|
||||
<button class="btn btn-primary" :disabled="savingFeatureFlags" @click="saveFeatureFlags">
|
||||
{{ savingFeatureFlags ? "Saving..." : "Save Feature Flags" }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<AdminSpaceModal v-if="showSpaceModal && selectedSpace" :space="selectedSpace" :users="users" @close="showSpaceModal = false" @saved="onSpaceSaved" @deleted="onSpaceDeleted" />
|
||||
|
||||
<teleport to="body">
|
||||
<div v-if="showGroupModal" class="modal fade show d-block" tabindex="-1" role="dialog" aria-modal="true" @click.self="closeGroupModal">
|
||||
<div class="modal-dialog modal-lg modal-dialog-centered" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ groupModalMode === "create" ? "Create Group" : "Edit Group" }}</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @click="closeGroupModal"></button>
|
||||
</div>
|
||||
<form @submit.prevent="submitGroupModal">
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Group name</label>
|
||||
<input v-model="groupModalForm.name" class="form-control" type="text" required :disabled="isEditingSystemGroup" />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Description</label>
|
||||
<input v-model="groupModalForm.description" class="form-control" type="text" :disabled="isEditingSystemGroup" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label">Permissions (one per line)</label>
|
||||
<textarea
|
||||
v-model="groupModalForm.permissionsText"
|
||||
class="form-control permissions-textarea"
|
||||
rows="10"
|
||||
placeholder="space.create space.project_docs.category.create space.project_docs.*"
|
||||
:disabled="isEditingSystemGroup"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="closeGroupModal">Cancel</button>
|
||||
<button v-if="!isEditingSystemGroup" type="submit" class="btn btn-primary" :disabled="submittingGroupModal">
|
||||
{{ submittingGroupModal ? "Saving..." : groupModalMode === "create" ? "Create Group" : "Save Changes" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showGroupModal" class="modal-backdrop fade show"></div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import apiClient from "../services/apiClient";
|
||||
import AdminSpaceModal from "../components/AdminSpaceModal.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const activeTab = ref("users");
|
||||
const error = ref("");
|
||||
const successMessage = ref("");
|
||||
|
||||
const users = ref([]);
|
||||
const loadingUsers = ref(false);
|
||||
|
||||
const groups = ref([]);
|
||||
const loadingGroups = ref(false);
|
||||
const showGroupModal = ref(false);
|
||||
const groupModalMode = ref("create");
|
||||
const editingGroupId = ref("");
|
||||
const submittingGroupModal = ref(false);
|
||||
const groupModalForm = ref({
|
||||
name: "",
|
||||
description: "",
|
||||
permissionsText: "",
|
||||
});
|
||||
|
||||
const spaces = ref([]);
|
||||
const loadingSpaces = ref(false);
|
||||
const showSpaceModal = ref(false);
|
||||
const selectedSpace = ref(null);
|
||||
|
||||
const providers = ref([]);
|
||||
const loadingProviders = ref(false);
|
||||
const submittingProvider = ref(false);
|
||||
const providerForm = ref({
|
||||
name: "",
|
||||
type: "oidc",
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
authorization_url: "",
|
||||
token_url: "",
|
||||
userinfo_url: "",
|
||||
id_token_claim: "id_token",
|
||||
scopes: "openid, profile, email",
|
||||
is_active: true,
|
||||
});
|
||||
|
||||
const loadingFeatureFlags = ref(false);
|
||||
const savingFeatureFlags = ref(false);
|
||||
const featureFlagsForm = ref({
|
||||
registration_enabled: true,
|
||||
provider_login_enabled: true,
|
||||
public_sharing_enabled: true,
|
||||
});
|
||||
|
||||
const clearMessages = () => {
|
||||
error.value = "";
|
||||
successMessage.value = "";
|
||||
};
|
||||
|
||||
const formatDate = (iso) => {
|
||||
if (!iso) return "";
|
||||
return new Date(iso).toLocaleDateString();
|
||||
};
|
||||
|
||||
const loadUsers = async () => {
|
||||
loadingUsers.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/admin/users");
|
||||
users.value = res.data.users || [];
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load users.";
|
||||
} finally {
|
||||
loadingUsers.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserGroups = async (userId, groupIds) => {
|
||||
clearMessages();
|
||||
try {
|
||||
const response = await apiClient.put(`/api/v1/admin/users/${userId}/groups`, { group_ids: groupIds });
|
||||
const updatedUser = response.data;
|
||||
const userIndex = users.value.findIndex((user) => user.id === userId);
|
||||
if (userIndex !== -1) {
|
||||
users.value[userIndex] = { ...users.value[userIndex], ...updatedUser };
|
||||
}
|
||||
successMessage.value = "User groups updated.";
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to update user groups.";
|
||||
}
|
||||
};
|
||||
|
||||
const resetGroupModalForm = () => {
|
||||
groupModalForm.value = {
|
||||
name: "",
|
||||
description: "",
|
||||
permissionsText: "",
|
||||
};
|
||||
};
|
||||
|
||||
const isEditingSystemGroup = computed(() => {
|
||||
if (groupModalMode.value !== "edit") {
|
||||
return false;
|
||||
}
|
||||
const group = groups.value.find((item) => item.id === editingGroupId.value);
|
||||
return !!group?.is_system;
|
||||
});
|
||||
|
||||
const splitPermissionsByNewline = (raw) =>
|
||||
(raw || "")
|
||||
.split(/\r?\n/)
|
||||
.map((permission) => permission.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const openCreateGroupModal = () => {
|
||||
groupModalMode.value = "create";
|
||||
editingGroupId.value = "";
|
||||
resetGroupModalForm();
|
||||
showGroupModal.value = true;
|
||||
};
|
||||
|
||||
const openEditGroupModal = (group) => {
|
||||
groupModalMode.value = "edit";
|
||||
editingGroupId.value = group.id;
|
||||
groupModalForm.value = {
|
||||
name: group.name || "",
|
||||
description: group.description || "",
|
||||
permissionsText: (group.permissions || []).join("\n"),
|
||||
};
|
||||
showGroupModal.value = true;
|
||||
};
|
||||
|
||||
const closeGroupModal = () => {
|
||||
showGroupModal.value = false;
|
||||
submittingGroupModal.value = false;
|
||||
};
|
||||
|
||||
const loadGroups = async () => {
|
||||
loadingGroups.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/admin/groups");
|
||||
groups.value = res.data.groups || [];
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load groups.";
|
||||
} finally {
|
||||
loadingGroups.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const submitGroupModal = async () => {
|
||||
submittingGroupModal.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const payload = {
|
||||
name: groupModalForm.value.name,
|
||||
description: groupModalForm.value.description,
|
||||
permissions: splitPermissionsByNewline(groupModalForm.value.permissionsText),
|
||||
};
|
||||
|
||||
if (groupModalMode.value === "create") {
|
||||
await apiClient.post("/api/v1/admin/groups", payload);
|
||||
successMessage.value = "Group created.";
|
||||
} else {
|
||||
await apiClient.put(`/api/v1/admin/groups/${editingGroupId.value}`, payload);
|
||||
successMessage.value = "Group updated.";
|
||||
}
|
||||
|
||||
closeGroupModal();
|
||||
resetGroupModalForm();
|
||||
await Promise.all([loadGroups(), loadUsers()]);
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || `Failed to ${groupModalMode.value === "create" ? "create" : "update"} group.`;
|
||||
} finally {
|
||||
submittingGroupModal.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadSpaces = async () => {
|
||||
loadingSpaces.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/admin/spaces");
|
||||
spaces.value = res.data.spaces || [];
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load spaces.";
|
||||
} finally {
|
||||
loadingSpaces.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openSpaceModal = (space) => {
|
||||
selectedSpace.value = { ...space };
|
||||
showSpaceModal.value = true;
|
||||
};
|
||||
|
||||
const onSpaceSaved = (updatedSpace) => {
|
||||
const index = spaces.value.findIndex((space) => space.id === updatedSpace.id);
|
||||
if (index !== -1) {
|
||||
spaces.value[index] = { ...spaces.value[index], ...updatedSpace };
|
||||
}
|
||||
selectedSpace.value = { ...updatedSpace };
|
||||
successMessage.value = "Space updated.";
|
||||
};
|
||||
|
||||
const onSpaceDeleted = (deletedSpace) => {
|
||||
spaces.value = spaces.value.filter((space) => space.id !== deletedSpace.id);
|
||||
showSpaceModal.value = false;
|
||||
selectedSpace.value = null;
|
||||
successMessage.value = `Space "${deletedSpace.name}" deleted.`;
|
||||
};
|
||||
|
||||
const buildCallbackUrl = (providerId) => `${apiClient.defaults.baseURL}/api/v1/auth/providers/${providerId}/callback`;
|
||||
|
||||
const resetProviderForm = () => {
|
||||
providerForm.value = {
|
||||
name: "",
|
||||
type: "oidc",
|
||||
client_id: "",
|
||||
client_secret: "",
|
||||
authorization_url: "",
|
||||
token_url: "",
|
||||
userinfo_url: "",
|
||||
id_token_claim: "id_token",
|
||||
scopes: "openid, profile, email",
|
||||
is_active: true,
|
||||
};
|
||||
};
|
||||
|
||||
const loadProviders = async () => {
|
||||
loadingProviders.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/auth/providers");
|
||||
providers.value = res.data.providers || [];
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load providers.";
|
||||
} finally {
|
||||
loadingProviders.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const createProvider = async () => {
|
||||
submittingProvider.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
await apiClient.post("/api/v1/admin/auth/providers", {
|
||||
...providerForm.value,
|
||||
scopes: providerForm.value.scopes
|
||||
.split(",")
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean),
|
||||
});
|
||||
successMessage.value = "Provider added.";
|
||||
resetProviderForm();
|
||||
await loadProviders();
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to create provider.";
|
||||
} finally {
|
||||
submittingProvider.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const loadFeatureFlags = async () => {
|
||||
loadingFeatureFlags.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/admin/feature-flags");
|
||||
featureFlagsForm.value = {
|
||||
registration_enabled: !!res.data.registration_enabled,
|
||||
provider_login_enabled: !!res.data.provider_login_enabled,
|
||||
public_sharing_enabled: !!res.data.public_sharing_enabled,
|
||||
};
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load feature flags.";
|
||||
} finally {
|
||||
loadingFeatureFlags.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveFeatureFlags = async () => {
|
||||
savingFeatureFlags.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.put("/api/v1/admin/feature-flags", featureFlagsForm.value);
|
||||
featureFlagsForm.value = {
|
||||
registration_enabled: !!res.data.registration_enabled,
|
||||
provider_login_enabled: !!res.data.provider_login_enabled,
|
||||
public_sharing_enabled: !!res.data.public_sharing_enabled,
|
||||
};
|
||||
successMessage.value = "Feature flags updated.";
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to update feature flags.";
|
||||
} finally {
|
||||
savingFeatureFlags.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
await Promise.all([loadUsers(), loadGroups(), loadSpaces(), loadProviders(), loadFeatureFlags()]);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.admin-page {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.admin-section {
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.permissions-textarea {
|
||||
font-family: "Courier New", monospace;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user