Implement Keycloak authentication integration and unify user management
This commit is contained in:
44
pages/auth/callback.vue
Normal file
44
pages/auth/callback.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<v-app>
|
||||
<v-main>
|
||||
<v-container class="fill-height">
|
||||
<v-row align="center" justify="center">
|
||||
<v-col cols="12" sm="8" md="4">
|
||||
<v-card class="pa-6">
|
||||
<v-card-text class="text-center">
|
||||
<v-progress-circular
|
||||
indeterminate
|
||||
color="primary"
|
||||
size="64"
|
||||
class="mb-4"
|
||||
/>
|
||||
<h2 class="text-h5 mb-2">Authenticating...</h2>
|
||||
<p class="text-body-2 text-medium-emphasis">
|
||||
Please wait while we complete your login.
|
||||
</p>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</v-main>
|
||||
</v-app>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// This page handles the OAuth callback from Keycloak
|
||||
// The nuxt-openid-connect module will automatically process the callback
|
||||
// and redirect to the originally requested page or default redirect
|
||||
|
||||
definePageMeta({
|
||||
auth: false, // This page should be accessible without authentication
|
||||
layout: false // Use minimal layout for callback page
|
||||
});
|
||||
|
||||
// The OIDC module handles the callback automatically
|
||||
// If you need custom logic after successful authentication, you can add it here
|
||||
onMounted(() => {
|
||||
// Optional: Add any custom post-authentication logic here
|
||||
console.log('OAuth callback page mounted');
|
||||
});
|
||||
</script>
|
||||
@@ -20,10 +20,16 @@
|
||||
<v-list lines="two">
|
||||
<v-list-item
|
||||
v-if="user"
|
||||
:title="`${user.first_name} ${user.last_name}`"
|
||||
:title="user.name"
|
||||
:subtitle="user.email"
|
||||
prepend-icon="mdi-account"
|
||||
/>
|
||||
>
|
||||
<template #append>
|
||||
<v-chip v-if="user.tier && user.tier !== 'basic'" size="small" color="primary">
|
||||
{{ user.tier }}
|
||||
</v-chip>
|
||||
</template>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
@click="logOut"
|
||||
title="Log out"
|
||||
@@ -65,8 +71,7 @@ definePageMeta({
|
||||
});
|
||||
|
||||
const { mdAndDown } = useDisplay();
|
||||
const { logout } = useDirectusAuth();
|
||||
const user = useDirectusUser();
|
||||
const { user, logout, authSource } = useUnifiedAuth();
|
||||
const tags = usePortalTags();
|
||||
|
||||
const drawer = ref(false);
|
||||
|
||||
136
pages/dashboard/auth-test.vue
Normal file
136
pages/dashboard/auth-test.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-row>
|
||||
<v-col cols="12">
|
||||
<h1 class="text-h4 mb-4">Authentication Test</h1>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row>
|
||||
<v-col cols="12" md="6">
|
||||
<v-card>
|
||||
<v-card-title>Current Authentication Status</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon :color="isAuthenticated ? 'success' : 'error'">
|
||||
{{ isAuthenticated ? 'mdi-check-circle' : 'mdi-close-circle' }}
|
||||
</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Authentication Status</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ isAuthenticated ? 'Authenticated' : 'Not Authenticated' }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="authSource">
|
||||
<template #prepend>
|
||||
<v-icon>mdi-shield-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Auth Source</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ authSource }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="12" md="6">
|
||||
<v-card v-if="user">
|
||||
<v-card-title>User Information</v-card-title>
|
||||
<v-card-text>
|
||||
<v-list>
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-identifier</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>User ID</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ user.id }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-email</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Email</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ user.email }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-account</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Name</v-list-item-title>
|
||||
<v-list-item-subtitle>{{ user.name }}</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item>
|
||||
<template #prepend>
|
||||
<v-icon>mdi-medal</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Tier</v-list-item-title>
|
||||
<v-list-item-subtitle>
|
||||
<v-chip :color="user.tier === 'admin' ? 'error' : 'primary'" size="small">
|
||||
{{ user.tier || 'basic' }}
|
||||
</v-chip>
|
||||
</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item v-if="isAdmin">
|
||||
<template #prepend>
|
||||
<v-icon color="error">mdi-shield-crown</v-icon>
|
||||
</template>
|
||||
<v-list-item-title>Admin Status</v-list-item-title>
|
||||
<v-list-item-subtitle>User has admin privileges</v-list-item-subtitle>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row v-if="user">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>Raw User Data</v-card-title>
|
||||
<v-card-text>
|
||||
<v-expansion-panels>
|
||||
<v-expansion-panel>
|
||||
<v-expansion-panel-title>Click to view raw data</v-expansion-panel-title>
|
||||
<v-expansion-panel-text>
|
||||
<pre class="text-caption">{{ JSON.stringify(user.raw, null, 2) }}</pre>
|
||||
</v-expansion-panel-text>
|
||||
</v-expansion-panel>
|
||||
</v-expansion-panels>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row class="mt-4">
|
||||
<v-col cols="12">
|
||||
<v-card>
|
||||
<v-card-title>Test Actions</v-card-title>
|
||||
<v-card-text>
|
||||
<v-btn color="error" @click="testLogout" prepend-icon="mdi-logout">
|
||||
Test Logout
|
||||
</v-btn>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const { user, isAuthenticated, authSource, isAdmin, logout } = useUnifiedAuth();
|
||||
const router = useRouter();
|
||||
|
||||
const testLogout = async () => {
|
||||
await logout();
|
||||
// The middleware should redirect to login automatically
|
||||
};
|
||||
|
||||
useHead({
|
||||
title: 'Authentication Test'
|
||||
});
|
||||
</script>
|
||||
165
pages/login.vue
165
pages/login.vue
@@ -4,70 +4,96 @@
|
||||
<v-container class="fill-height" fluid>
|
||||
<v-row align="center" justify="center" class="fill-height">
|
||||
<v-col cols="12" class="d-flex flex-column align-center">
|
||||
<v-card class="pa-6" rounded max-width="350" elevation="2">
|
||||
<v-form @submit.prevent="submit" v-model="valid">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="12">
|
||||
<v-img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" width="200" class="mb-3 mx-auto" />
|
||||
</v-col>
|
||||
<v-scroll-y-transition>
|
||||
<v-col v-if="errorThrown" cols="12" class="my-3">
|
||||
<v-alert
|
||||
text="Invalid email address or password"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
/>
|
||||
</v-col>
|
||||
</v-scroll-y-transition>
|
||||
<v-col cols="12">
|
||||
<v-row dense>
|
||||
<v-col cols="12" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="emailAddress"
|
||||
placeholder="Email address"
|
||||
:disabled="loading"
|
||||
:rules="[
|
||||
(value) => !!value || 'Must not be empty',
|
||||
(value) =>
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ||
|
||||
'Invalid email address',
|
||||
]"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
autofocus
|
||||
/>
|
||||
</v-col>
|
||||
<v-card class="pa-6" rounded max-width="450" elevation="2">
|
||||
<v-row no-gutters>
|
||||
<v-col cols="12">
|
||||
<v-img src="/Port_Nimara_Logo_2_Colour_New_Transparent.png" width="200" class="mb-3 mx-auto" />
|
||||
</v-col>
|
||||
|
||||
<!-- Keycloak SSO Login -->
|
||||
<v-col cols="12" class="mb-4">
|
||||
<v-btn
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
@click="loginWithKeycloak"
|
||||
prepend-icon="mdi-shield-account"
|
||||
>
|
||||
Login with Single Sign-On
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<!-- Divider -->
|
||||
<v-col cols="12">
|
||||
<v-divider class="my-4">
|
||||
<span class="text-caption">OR</span>
|
||||
</v-divider>
|
||||
</v-col>
|
||||
|
||||
<!-- Existing Directus Login Form -->
|
||||
<v-col cols="12">
|
||||
<v-form @submit.prevent="submit" v-model="valid">
|
||||
<v-row no-gutters>
|
||||
<v-scroll-y-transition>
|
||||
<v-col v-if="errorThrown" cols="12" class="my-3">
|
||||
<v-alert
|
||||
text="Invalid email address or password"
|
||||
color="error"
|
||||
variant="tonal"
|
||||
/>
|
||||
</v-col>
|
||||
</v-scroll-y-transition>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
@click:append-inner="passwordVisible = !passwordVisible"
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
:disabled="loading"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="
|
||||
passwordVisible ? 'mdi-eye' : 'mdi-eye-off'
|
||||
"
|
||||
:rules="[(value) => !!value || 'Must not be empty']"
|
||||
autocomplete="current-password"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
text="Log in"
|
||||
:disabled="!valid"
|
||||
:loading="loading"
|
||||
type="submit"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
/>
|
||||
<v-row dense>
|
||||
<v-col cols="12" class="mt-4">
|
||||
<v-text-field
|
||||
v-model="emailAddress"
|
||||
placeholder="Email address"
|
||||
:disabled="loading"
|
||||
:rules="[
|
||||
(value) => !!value || 'Must not be empty',
|
||||
(value) =>
|
||||
/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ||
|
||||
'Invalid email address',
|
||||
]"
|
||||
variant="outlined"
|
||||
type="email"
|
||||
autofocus
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-text-field
|
||||
@click:append-inner="passwordVisible = !passwordVisible"
|
||||
v-model="password"
|
||||
placeholder="Password"
|
||||
:disabled="loading"
|
||||
:type="passwordVisible ? 'text' : 'password'"
|
||||
:append-inner-icon="
|
||||
passwordVisible ? 'mdi-eye' : 'mdi-eye-off'
|
||||
"
|
||||
:rules="[(value) => !!value || 'Must not be empty']"
|
||||
autocomplete="current-password"
|
||||
variant="outlined"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="12">
|
||||
<v-btn
|
||||
text="Log in"
|
||||
:disabled="!valid"
|
||||
:loading="loading"
|
||||
type="submit"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
size="large"
|
||||
block
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-form>
|
||||
</v-form>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-card>
|
||||
|
||||
<!-- PWA Install Banner -->
|
||||
@@ -80,18 +106,33 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
// Define page meta for public access
|
||||
definePageMeta({
|
||||
auth: false
|
||||
});
|
||||
|
||||
// Directus auth
|
||||
const { login } = useDirectusAuth();
|
||||
|
||||
// OIDC auth for Keycloak
|
||||
const oidc = useOidc();
|
||||
|
||||
const loading = ref(false);
|
||||
const errorThrown = ref(false);
|
||||
|
||||
const emailAddress = ref();
|
||||
|
||||
const password = ref();
|
||||
const passwordVisible = ref(false);
|
||||
|
||||
const valid = ref(false);
|
||||
|
||||
// Keycloak login function
|
||||
const loginWithKeycloak = () => {
|
||||
// Redirect to dashboard after login
|
||||
oidc.login('/dashboard');
|
||||
};
|
||||
|
||||
// Directus login function
|
||||
const submit = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
Reference in New Issue
Block a user