first commit

This commit is contained in:
domrichardson
2026-03-24 16:03:04 +00:00
commit df40cc57e1
80 changed files with 16766 additions and 0 deletions

View 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>