first commit
This commit is contained in:
265
frontend/src/components/ManageAuthProvidersModal.vue
Normal file
265
frontend/src/components/ManageAuthProvidersModal.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<teleport to="body">
|
||||
<div class="modal-backdrop-custom" @click.self="$emit('close')">
|
||||
<div class="modal-panel">
|
||||
<div class="provider-modal-header">
|
||||
<div>
|
||||
<h5 class="provider-modal-title mb-1">Identity Providers</h5>
|
||||
<p class="text-muted mb-0">Configure OAuth2 and OIDC buttons for the login page.</p>
|
||||
</div>
|
||||
<button type="button" class="btn-close provider-modal-close" @click="$emit('close')"></button>
|
||||
</div>
|
||||
|
||||
<div class="provider-modal-body">
|
||||
<div v-if="error" class="alert alert-danger">{{ error }}</div>
|
||||
<div v-if="successMessage" class="alert alert-success">{{ successMessage }}</div>
|
||||
|
||||
<section class="provider-section">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<h6 class="mb-0">Configured Providers</h6>
|
||||
<button class="btn btn-sm btn-outline-secondary" :disabled="loading" @click="loadProviders">Refresh</button>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" 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">
|
||||
<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>
|
||||
</section>
|
||||
|
||||
<section class="provider-section">
|
||||
<h6 class="mb-3">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="form.name" type="text" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Provider Type</label>
|
||||
<select v-model="form.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="form.client_id" type="text" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Client Secret</label>
|
||||
<input v-model="form.client_secret" type="password" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Authorization URL</label>
|
||||
<input v-model="form.authorization_url" type="url" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">Token URL</label>
|
||||
<input v-model="form.token_url" type="url" class="form-control" required />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">UserInfo URL</label>
|
||||
<input v-model="form.userinfo_url" type="url" class="form-control" placeholder="Optional when id_token contains profile claims" />
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<label class="form-label">ID Token Field</label>
|
||||
<input v-model="form.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="form.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="form.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 gap-2">
|
||||
<button type="button" class="btn btn-outline-secondary" @click="$emit('close')">Close</button>
|
||||
<button type="submit" class="btn btn-primary" :disabled="submitting">
|
||||
{{ submitting ? "Saving..." : "Add Provider" }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from "vue";
|
||||
import apiClient from "../services/apiClient";
|
||||
|
||||
defineEmits(["close"]);
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
const error = ref("");
|
||||
const successMessage = ref("");
|
||||
const providers = ref([]);
|
||||
const form = 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 loadProviders = async () => {
|
||||
loading.value = true;
|
||||
error.value = "";
|
||||
|
||||
try {
|
||||
const response = await apiClient.get("/api/v1/auth/providers");
|
||||
providers.value = response.data.providers || [];
|
||||
} catch (err) {
|
||||
error.value = err.response?.data || err.message;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const resetForm = () => {
|
||||
form.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 buildCallbackUrl = (providerId) => `${apiClient.defaults.baseURL}/api/v1/auth/providers/${providerId}/callback`;
|
||||
|
||||
const createProvider = async () => {
|
||||
submitting.value = true;
|
||||
error.value = "";
|
||||
successMessage.value = "";
|
||||
|
||||
try {
|
||||
await apiClient.post("/api/v1/auth/providers", {
|
||||
...form.value,
|
||||
scopes: form.value.scopes
|
||||
.split(",")
|
||||
.map((scope) => scope.trim())
|
||||
.filter(Boolean),
|
||||
});
|
||||
successMessage.value = "Provider added.";
|
||||
resetForm();
|
||||
await loadProviders();
|
||||
} catch (err) {
|
||||
error.value = err.response?.data || err.message;
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(loadProviders);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-backdrop-custom {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(15, 23, 42, 0.45);
|
||||
backdrop-filter: blur(2px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 2000;
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.modal-panel {
|
||||
width: min(920px, 100%);
|
||||
max-height: min(92vh, 980px);
|
||||
background: #fff;
|
||||
border: 1px solid #dbe3ee;
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 1rem 3rem rgba(0, 0, 0, 0.2);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.provider-modal-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: linear-gradient(180deg, #f8fafc 0%, #ffffff 100%);
|
||||
}
|
||||
|
||||
.provider-modal-title {
|
||||
margin: 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.provider-modal-close {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.provider-modal-body {
|
||||
padding: 1rem 1.25rem 1.25rem;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.provider-section {
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 10px;
|
||||
background: #f8fafc;
|
||||
padding: 0.9rem;
|
||||
}
|
||||
|
||||
.provider-list {
|
||||
max-height: 220px;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.modal-backdrop-custom {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.provider-modal-header,
|
||||
.provider-modal-body {
|
||||
padding-left: 0.85rem;
|
||||
padding-right: 0.85rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user