feat: Updated admin panel providers list & modal
This commit is contained in:
33
frontend/eslint.config.js
Normal file
33
frontend/eslint.config.js
Normal file
@@ -0,0 +1,33 @@
|
||||
import js from "@eslint/js";
|
||||
import globals from "globals";
|
||||
import pluginVue from "eslint-plugin-vue";
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: ["dist/**", "node_modules/**"],
|
||||
},
|
||||
js.configs.recommended,
|
||||
...pluginVue.configs["flat/essential"],
|
||||
{
|
||||
files: ["**/*.{js,mjs,cjs,vue}"],
|
||||
languageOptions: {
|
||||
ecmaVersion: "latest",
|
||||
sourceType: "module",
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
"no-unused-vars": [
|
||||
"warn",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
"no-console": "off",
|
||||
"vue/multi-word-component-names": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
1717
frontend/package-lock.json
generated
1717
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -7,7 +7,9 @@
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs"
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdi/font": "^7.4.47",
|
||||
@@ -23,8 +25,13 @@
|
||||
"vue-router": "^4.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.22.0",
|
||||
"@vitejs/plugin-vue": "^4.2.0",
|
||||
"@vue/test-utils": "^2.4.0",
|
||||
"eslint": "^9.22.0",
|
||||
"eslint-plugin-vue": "^9.32.0",
|
||||
"globals": "^16.0.0",
|
||||
"jsdom": "^29.0.1",
|
||||
"vite": "^4.3.0",
|
||||
"vitest": "^0.34.0"
|
||||
}
|
||||
|
||||
@@ -808,11 +808,6 @@ const createSpace = async (spaceData) => {
|
||||
await spaceStore.createSpace(spaceData);
|
||||
};
|
||||
|
||||
const createCategory = async (categoryData) => {
|
||||
showCreateCategoryModal.value = false;
|
||||
await spaceStore.createCategory(currentSpace.value.id, categoryData);
|
||||
};
|
||||
|
||||
const openCreateCategoryModal = () => {
|
||||
if (!canCreateCategories.value) {
|
||||
return;
|
||||
|
||||
@@ -60,6 +60,20 @@
|
||||
<label for="provider-active" class="form-check-label">Provider is active</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="mode === 'edit'" class="col-12">
|
||||
<div class="danger-zone border border-danger-subtle rounded p-3 mt-2">
|
||||
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center gap-2">
|
||||
<div>
|
||||
<div class="fw-semibold text-danger">Danger Zone</div>
|
||||
<div class="small text-muted">Permanently delete this provider configuration.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" :disabled="submitting || deleting" @click="emit('delete', props.provider)">
|
||||
<i class="mdi mdi-trash-can-outline me-1" aria-hidden="true"></i>Delete Provider
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
@@ -92,9 +106,13 @@ const props = defineProps({
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
deleting: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["close", "submit"]);
|
||||
const emit = defineEmits(["close", "submit", "delete"]);
|
||||
|
||||
const form = ref({
|
||||
name: "",
|
||||
|
||||
@@ -153,6 +153,12 @@
|
||||
<div v-else class="list-group">
|
||||
<div v-for="provider in providers" :key="provider.id" class="list-group-item d-flex justify-content-between align-items-center">
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<i
|
||||
class="mdi"
|
||||
:class="provider.is_active ? 'mdi-check-circle text-success' : 'mdi-close-circle text-secondary'"
|
||||
:title="provider.is_active ? 'Provider enabled' : 'Provider disabled'"
|
||||
aria-hidden="true"
|
||||
></i>
|
||||
<span class="fw-semibold">{{ provider.name }}</span>
|
||||
</div>
|
||||
<div class="d-flex gap-2">
|
||||
@@ -277,8 +283,10 @@
|
||||
:mode="providerModalMode"
|
||||
:provider="selectedProvider"
|
||||
:submitting="submittingProviderModal"
|
||||
:deleting="deletingProviderModal"
|
||||
@close="closeProviderModal"
|
||||
@submit="submitProviderModal"
|
||||
@delete="deleteProviderFromModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -335,6 +343,7 @@ const showProviderModal = ref(false);
|
||||
const providerModalMode = ref("create");
|
||||
const selectedProvider = ref(null);
|
||||
const submittingProviderModal = ref(false);
|
||||
const deletingProviderModal = ref(false);
|
||||
|
||||
const loadingFeatureFlags = ref(false);
|
||||
const savingFeatureFlags = ref(false);
|
||||
@@ -584,6 +593,7 @@ const openEditProviderModal = (provider) => {
|
||||
const closeProviderModal = () => {
|
||||
showProviderModal.value = false;
|
||||
submittingProviderModal.value = false;
|
||||
deletingProviderModal.value = false;
|
||||
selectedProvider.value = null;
|
||||
};
|
||||
|
||||
@@ -612,7 +622,7 @@ const loadProviders = async () => {
|
||||
loadingProviders.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
const res = await apiClient.get("/api/v1/auth/providers");
|
||||
const res = await apiClient.get("/api/v1/admin/auth/providers");
|
||||
providers.value = res.data.providers || [];
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to load providers.";
|
||||
@@ -621,18 +631,26 @@ const loadProviders = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteProvider = async (provider) => {
|
||||
const deleteProviderFromModal = async (provider) => {
|
||||
if (!provider?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Delete identity provider "${provider.name}"? This action cannot be undone.`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
deletingProviderModal.value = true;
|
||||
clearMessages();
|
||||
try {
|
||||
await apiClient.delete(`/api/v1/admin/auth/providers/${provider.id}`);
|
||||
providers.value = providers.value.filter((item) => item.id !== provider.id);
|
||||
successMessage.value = `Provider "${provider.name}" deleted.`;
|
||||
closeProviderModal();
|
||||
} catch (e) {
|
||||
error.value = e.response?.data || "Failed to delete provider.";
|
||||
} finally {
|
||||
deletingProviderModal.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { describe, it, expect, beforeEach, vi } from "vitest";
|
||||
import { useAuthStore } from "../../src/stores/authStore";
|
||||
// @vitest-environment node
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createPinia, setActivePinia } from "pinia";
|
||||
|
||||
vi.mock("../src/services/apiClient.js", () => ({
|
||||
default: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(() => Promise.resolve({})),
|
||||
},
|
||||
}));
|
||||
|
||||
import apiClient from "../src/services/apiClient.js";
|
||||
import { useAuthStore } from "../src/stores/authStore.js";
|
||||
|
||||
describe("Auth Store", () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should initialize with no user", () => {
|
||||
@@ -13,27 +25,76 @@ describe("Auth Store", () => {
|
||||
expect(store.user).toBeNull();
|
||||
});
|
||||
|
||||
it("should store user data on login", () => {
|
||||
it("should store user data with setSession", () => {
|
||||
const store = useAuthStore();
|
||||
|
||||
// Mock user data
|
||||
const mockUser = {
|
||||
id: "123",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
permissions: ["space.demo.note.create"],
|
||||
};
|
||||
|
||||
// In a real test, you'd mock the API call
|
||||
// For now, just test the store structure
|
||||
expect(store.user).toBeNull();
|
||||
store.setSession({ user: mockUser });
|
||||
|
||||
expect(store.isAuthenticated).toBe(true);
|
||||
expect(store.user).toEqual(mockUser);
|
||||
expect(store.hasPermission("space.demo.note.create")).toBe(true);
|
||||
});
|
||||
|
||||
it("should clear user data on logout", () => {
|
||||
it("should login and persist returned user", async () => {
|
||||
const store = useAuthStore();
|
||||
apiClient.post.mockResolvedValueOnce({
|
||||
data: {
|
||||
user: {
|
||||
id: "123",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
permissions: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await store.login(" test@example.com ", "password123");
|
||||
|
||||
expect(apiClient.post).toHaveBeenCalledWith("/api/v1/auth/login", {
|
||||
email: "test@example.com",
|
||||
password: "password123",
|
||||
});
|
||||
expect(result.user.username).toBe("testuser");
|
||||
expect(store.user?.username).toBe("testuser");
|
||||
});
|
||||
|
||||
it("should clear user data on logout", async () => {
|
||||
const store = useAuthStore();
|
||||
store.setSession({
|
||||
user: {
|
||||
id: "123",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
permissions: ["space.demo.settings.delete"],
|
||||
},
|
||||
});
|
||||
|
||||
store.logout();
|
||||
|
||||
expect(store.isAuthenticated).toBe(false);
|
||||
expect(store.user).toBeNull();
|
||||
expect(store.accessToken).toBeNull();
|
||||
expect(apiClient.post).toHaveBeenCalledWith("/api/v1/auth/logout");
|
||||
});
|
||||
|
||||
it("should evaluate space permissions using the space permission key", () => {
|
||||
const store = useAuthStore();
|
||||
store.setSession({
|
||||
user: {
|
||||
id: "123",
|
||||
email: "test@example.com",
|
||||
username: "testuser",
|
||||
permissions: ["space.docs.settings.delete", "space.*.note.create"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(store.hasSpacePermission({ permission_key: "docs" }, "settings.delete")).toBe(true);
|
||||
expect(store.hasSpacePermission({ permission_key: "docs" }, "note.create")).toBe(true);
|
||||
expect(store.hasSpacePermission({ permission_key: "docs" }, "note.delete")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user