major: Migration from better-auth to keycloak
This commit is contained in:
@@ -1,92 +0,0 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Accept Invitation" :ui="{ right: 'gap-3' }">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right />
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left />
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-2xl mx-auto">
|
||||
<UPageCard title="Accept Invitation" description="Accept or decline the invitation." class="mb-6">
|
||||
<div v-if="invitation && !error">
|
||||
<div v-if="invitationStatus === 'pending'" class="space-y-4">
|
||||
<p>
|
||||
<strong>{{ invitation.inviterEmail }}</strong> has invited you to join
|
||||
<strong>{{ invitation.organizationName }}</strong
|
||||
>.
|
||||
</p>
|
||||
<p>
|
||||
This invitation was sent to <strong>{{ invitation.email }}</strong
|
||||
>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="invitationStatus === 'accepted'" class="space-y-4 text-center">
|
||||
<div class="flex items-center justify-center w-16 h-16 mx-auto bg-green-100 rounded-full">
|
||||
<UIcon name="i-lucide-check" class="w-8 h-8 text-green-600" />
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold">Welcome to {{ invitation.organizationName }}!</h2>
|
||||
<p>You've successfully joined the organization. We're excited to have you on board!</p>
|
||||
</div>
|
||||
|
||||
<div v-if="invitationStatus === 'rejected'" class="space-y-4 text-center">
|
||||
<div class="flex items-center justify-center w-16 h-16 mx-auto bg-red-100 rounded-full">
|
||||
<UIcon name="i-lucide-x" class="w-8 h-8 text-red-600" />
|
||||
</div>
|
||||
<h2 class="text-2xl font-bold">Invitation Declined</h2>
|
||||
<p>You've declined the invitation to join {{ invitation.organizationName }}.</p>
|
||||
</div>
|
||||
|
||||
<div v-if="invitationStatus === 'pending'" class="flex justify-between mt-6">
|
||||
<UButton variant="soft" color="neutral" @click="handleReject">Decline</UButton>
|
||||
<UButton color="primary" @click="handleAccept">Accept Invitation</UButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="!invitation && !error" class="space-y-4">
|
||||
<USkeleton class="w-1/3 h-6" />
|
||||
<USkeleton class="w-full h-4" />
|
||||
<USkeleton class="w-2/3 h-4" />
|
||||
<USkeleton class="w-24 h-10 ml-auto" />
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<p class="text-red-600 font-medium">{{ error }}</p>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { CustomInvitation } from '~/types/auth'
|
||||
|
||||
const invitationId = useRoute().params.id as string
|
||||
|
||||
const { acceptInvitation, rejectInvitation, getInvitation } = useOrganizationStore()
|
||||
const invitation = ref<CustomInvitation>(null)
|
||||
const invitationStatus = ref<'pending' | 'accepted' | 'rejected'>('pending')
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
async function handleAccept() {
|
||||
await acceptInvitation(invitationId)
|
||||
}
|
||||
|
||||
async function handleReject() {
|
||||
await rejectInvitation(invitationId)
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
invitation.value = await getInvitation(invitationId)
|
||||
})
|
||||
</script>
|
||||
@@ -1,191 +0,0 @@
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Administration" :ui="{ right: 'gap-3' }">
|
||||
<template #leading>
|
||||
<UDashboardSidebarCollapse />
|
||||
</template>
|
||||
|
||||
<template #right />
|
||||
</UDashboardNavbar>
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left />
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-2xl mx-auto">
|
||||
<UPageCard
|
||||
title="Organization Selection"
|
||||
description="Choose an organization or create a new one."
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="flex justify-between items-center">
|
||||
<USelect
|
||||
v-model="selectedOrganizationId"
|
||||
:items="labeledOrganizations"
|
||||
value-key="value"
|
||||
placeholder="Select organization"
|
||||
class="w-64"
|
||||
/>
|
||||
<CreateOrganizationModal />
|
||||
</div>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard
|
||||
title="Organization Overview"
|
||||
description="View your current organization and its details."
|
||||
class="mb-6"
|
||||
>
|
||||
<div class="flex gap-4 items-center">
|
||||
<UAvatar :src="activeOrganization?.logo || undefined" size="md" alt="Org Logo" class="rounded" />
|
||||
<div>
|
||||
<p class="font-semibold">{{ activeOrganization?.name }}</p>
|
||||
<p class="text-xs text-gray-500">{{ activeOrganization?.members?.length || 1 }} members</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-4">
|
||||
<UButton color="error" icon="i-lucide-trash" @click="deleteOrganization"> Delete Organization </UButton>
|
||||
</div>
|
||||
</UPageCard>
|
||||
|
||||
<UPageCard title="Members & Invitations" description="Manage team members and pending invites.">
|
||||
<div class="flex flex-col md:flex-row gap-8">
|
||||
<!-- Members -->
|
||||
<div class="flex-1">
|
||||
<p class="font-medium mb-2">Members</p>
|
||||
<div v-if="activeOrganizationMembers" class="space-y-2">
|
||||
<div
|
||||
v-for="member in activeOrganizationMembers"
|
||||
:key="member.id"
|
||||
class="flex justify-between items-center"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<UAvatar :src="member.user.image || undefined" size="sm" />
|
||||
<div>
|
||||
<p class="text-sm">{{ member.user.name }}</p>
|
||||
<p class="text-xs text-gray-500">{{ member.role }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="user && canRemove({ role: 'owner' }, member)">
|
||||
<UButton size="xs" color="error" @click="organizationStore.removeMember(member.id)">
|
||||
{{ member.user.id === user.id ? 'Leave' : 'Remove' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!activeOrganization?.id" class="flex items-center gap-2">
|
||||
<UAvatar :src="user?.image ?? undefined" />
|
||||
<div>
|
||||
<p class="text-sm">{{ user?.name }}</p>
|
||||
<p class="text-xs text-gray-500">Owner</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Invitations -->
|
||||
<div class="flex-1">
|
||||
<p class="font-medium mb-2">Invites</p>
|
||||
<div class="space-y-2">
|
||||
<template v-if="invitations.length > 0">
|
||||
<div
|
||||
v-for="invitation in invitations.filter((i: Invitation) => i.status === 'pending')"
|
||||
:key="invitation.id"
|
||||
class="flex justify-between items-center"
|
||||
>
|
||||
<div>
|
||||
<p class="text-sm">{{ invitation.email }}</p>
|
||||
<p class="text-xs text-gray-500">{{ invitation.role }}</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<UButton
|
||||
size="xs"
|
||||
color="error"
|
||||
:loading="isRevoking.includes(invitation.id)"
|
||||
@click="() => handleInvitationCancellation(invitation.id)"
|
||||
>
|
||||
Revoke
|
||||
</UButton>
|
||||
<UButton icon="i-lucide-copy" size="xs" @click="copy(getInviteLink(invitation.id))">
|
||||
{{ copied ? 'Copied!' : 'Copy' }}
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<p v-else class="text-sm text-gray-500">No active invitations</p>
|
||||
<p v-if="!activeOrganization?.id" class="text-xs text-gray-500">
|
||||
You can't invite members to your personal workspace.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end mt-6">
|
||||
<InviteMemberModal
|
||||
v-if="activeOrganization?.id"
|
||||
:organization="activeOrganization"
|
||||
@update="activeOrganization = $event"
|
||||
/>
|
||||
</div>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import type { Invitation } from 'better-auth/plugins'
|
||||
|
||||
const { copy, copied } = useClipboard()
|
||||
const { user } = useAuth()
|
||||
const organizationStore = useOrganizationStore()
|
||||
const { activeOrganization, activeOrganizationMembers, organizations, invitations } = storeToRefs(organizationStore)
|
||||
|
||||
const isRevoking = ref<string[]>([])
|
||||
|
||||
onMounted(async () => {
|
||||
await organizationStore.loadOrganizations()
|
||||
})
|
||||
|
||||
const labeledOrganizations = computed(() => organizations.value.map((org) => ({ label: org.name, value: org.id })))
|
||||
const selectedOrganizationId = computed({
|
||||
get() {
|
||||
return activeOrganization.value?.id
|
||||
},
|
||||
set(id: string) {
|
||||
organizationStore.setActiveOrganization(id)
|
||||
}
|
||||
})
|
||||
|
||||
function isAdminOrOwner(member: { role: string }) {
|
||||
return member.role === 'owner' || member.role === 'admin'
|
||||
}
|
||||
|
||||
function canRemove(current: { role: string }, target: { role: string }) {
|
||||
return target.role !== 'owner' && isAdminOrOwner(current)
|
||||
}
|
||||
|
||||
async function handleInvitationCancellation(invitationId: string) {
|
||||
isRevoking.value.push(invitationId)
|
||||
await organizationStore.cancelSentInvitation(invitationId)
|
||||
}
|
||||
|
||||
function getInviteLink(inviteId: string): string {
|
||||
return `${window.location.origin}/accept-invitation/${inviteId}`
|
||||
}
|
||||
|
||||
async function deleteOrganization() {
|
||||
if (!activeOrganization.value?.id) return
|
||||
|
||||
const confirmed = confirm(
|
||||
`Are you sure you want to delete the organization "${activeOrganization.value.name}"? This cannot be undone.`
|
||||
)
|
||||
|
||||
if (!confirmed) return
|
||||
|
||||
await organizationStore.deleteOrganization()
|
||||
}
|
||||
</script>
|
||||
@@ -80,7 +80,9 @@ import type { StepperItem } from '@nuxt/ui'
|
||||
|
||||
const { getApplicationFormById, updateApplicationForm, submitApplicationForm } = useApplicationForm()
|
||||
const route = useRoute()
|
||||
const { user } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
const { user } = storeToRefs(userStore)
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
definePageMeta({
|
||||
|
||||
19
legalconsenthub/pages/callback.vue
Normal file
19
legalconsenthub/pages/callback.vue
Normal file
@@ -0,0 +1,19 @@
|
||||
<template>
|
||||
<h1>Authentication callback processing...</h1>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useKeycloak } from '~/composables/useKeycloak'
|
||||
|
||||
const { userManager } = useKeycloak()
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
const user = await userManager.signinRedirectCallback()
|
||||
console.log('User logged in', user)
|
||||
await navigateTo('/')
|
||||
} catch (e) {
|
||||
console.error('Error during login', e)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
<template #body>
|
||||
<div class="flex flex-col gap-4 sm:gap-6 lg:gap-12 w-full lg:max-w-4xl mx-auto">
|
||||
<div v-if="!canCreateApplicationForm" class="text-center py-12">
|
||||
<div v-if="!true" class="text-center py-12">
|
||||
<UIcon name="i-lucide-shield-x" class="w-16 h-16 mx-auto text-red-400 mb-4" />
|
||||
<h2 class="text-2xl font-semibold text-gray-700 mb-2">Keine Berechtigung</h2>
|
||||
<p class="text-gray-500 mb-4">Sie haben keine Berechtigung zum Erstellen von Anträgen.</p>
|
||||
@@ -83,15 +83,19 @@ import { useApplicationFormValidator } from '~/composables/useApplicationFormVal
|
||||
import type { FormElementId } from '~/types/formElement'
|
||||
import type { StepperItem } from '@nuxt/ui'
|
||||
|
||||
const { getAllApplicationFormTemplates } = useApplicationFormTemplate()
|
||||
const { getAllApplicationFormTemplates } = await useApplicationFormTemplate()
|
||||
const { createApplicationForm, submitApplicationForm } = useApplicationForm()
|
||||
const { validateFormElements, getHighestComplianceStatus } = useApplicationFormValidator()
|
||||
const { selectedOrganization } = useAuth()
|
||||
const { canCreateApplicationForm, getCurrentRoleInfo } = usePermissions()
|
||||
const userStore = useUserStore()
|
||||
const { selectedOrganization } = storeToRefs(userStore)
|
||||
const toast = useToast()
|
||||
|
||||
// Get current role information for display
|
||||
const currentRoleInfo = computed(() => getCurrentRoleInfo())
|
||||
const currentRoleInfo = {
|
||||
name: 'Mitarbeiter',
|
||||
description: 'Sie können Anträge erstellen und bearbeiten.',
|
||||
color: 'info'
|
||||
}
|
||||
|
||||
const stepper = useTemplateRef('stepper')
|
||||
const activeStepperItemIndex = ref<number>(0)
|
||||
@@ -196,6 +200,7 @@ async function prepareAndCreateApplicationForm() {
|
||||
return null
|
||||
}
|
||||
|
||||
console.log('selectedOrganization', selectedOrganization.value)
|
||||
applicationFormTemplate.value.organizationId = selectedOrganization.value?.id ?? ''
|
||||
|
||||
return await createApplicationForm(applicationFormTemplate.value)
|
||||
|
||||
@@ -78,10 +78,12 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ApplicationFormDto, PagedApplicationFormDto } from '~/.api-client'
|
||||
import type { Organization } from '~/types/keycloak'
|
||||
|
||||
const { getAllApplicationForms, deleteApplicationFormById } = useApplicationForm()
|
||||
const route = useRoute()
|
||||
const { organizations, selectedOrganization } = useAuth()
|
||||
const userStore = useUserStore()
|
||||
const { organizations, selectedOrganization } = storeToRefs(userStore)
|
||||
|
||||
// Inject notification state from layout
|
||||
const { isNotificationsSlideoverOpen, unreadCount } = inject('notificationState', {
|
||||
@@ -117,7 +119,7 @@ const selectedOrganizationId = computed({
|
||||
},
|
||||
set(item) {
|
||||
// TODO: USelect triggers multiple times after single selection
|
||||
selectedOrganization.value = organizations.value.find((i) => i.id === item) ?? null
|
||||
selectedOrganization.value = organizations.value.find((i: Organization) => i.id === item) ?? null
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -1,80 +1,42 @@
|
||||
<template>
|
||||
<UAuthForm
|
||||
:fields="fields"
|
||||
:schema="signInSchema"
|
||||
:providers="providers"
|
||||
title="Welcome back"
|
||||
icon="i-lucide-lock"
|
||||
@submit="onLoginSubmit"
|
||||
>
|
||||
<template #description>
|
||||
Don't have an account? <ULink to="/signup" class="text-primary-500 font-medium">Sign up</ULink>.
|
||||
</template>
|
||||
<UCard variant="subtle">
|
||||
<template #header>
|
||||
<div class="text-center">
|
||||
<UIcon name="i-lucide-lock" class="mx-auto h-16 w-16 text-primary-500 mb-6" />
|
||||
<h1 class="text-3xl font-bold text-gray-900 mb-2">
|
||||
Welcome
|
||||
</h1>
|
||||
<p class="text-gray-600">
|
||||
You will be redirected to Keycloak to authenticate
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #password-hint>
|
||||
<ULink to="/" class="text-primary-500 font-medium">Forgot password?</ULink>
|
||||
</template>
|
||||
<div class="text-center">
|
||||
<UButton
|
||||
color="primary"
|
||||
size="xl"
|
||||
icon="i-lucide-log-in"
|
||||
@click="handleSignIn"
|
||||
>
|
||||
Sign in with Keycloak
|
||||
</UButton>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
By signing in, you agree to our <ULink to="/" class="text-primary-500 font-medium">Terms of Service</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
<template #footer>
|
||||
<div class="text-center text-xs text-gray-500">
|
||||
By signing in, you agree to our terms of service
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import { signInSchema, type SignInSchema } from '~/types/schemas'
|
||||
|
||||
definePageMeta({ layout: 'auth' })
|
||||
definePageMeta({ auth: false, layout: 'auth' })
|
||||
|
||||
useSeoMeta({ title: 'Login' })
|
||||
|
||||
const toast = useToast()
|
||||
const { signIn } = useAuth()
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text' as const,
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password' as const,
|
||||
placeholder: 'Enter your password'
|
||||
},
|
||||
{
|
||||
name: 'remember',
|
||||
label: 'Remember me',
|
||||
type: 'checkbox' as const
|
||||
}
|
||||
]
|
||||
|
||||
const providers = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'Google', description: 'Login with Google' })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'GitHub', description: 'Login with GitHub' })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function onLoginSubmit(payload: FormSubmitEvent<SignInSchema>) {
|
||||
if (!payload.data.email || !payload.data.password) {
|
||||
alert('Bitte alle Felder ausfüllen')
|
||||
return
|
||||
}
|
||||
signIn(payload)
|
||||
function handleSignIn() {
|
||||
navigateTo('/auth/keycloak', { external: true })
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
<template>
|
||||
<UAuthForm
|
||||
:fields="fields"
|
||||
:schema="signUpSchema"
|
||||
:providers="providers"
|
||||
title="Create an account"
|
||||
:submit="{ label: 'Create account' }"
|
||||
@submit="onSignUpSubmit"
|
||||
>
|
||||
<template #description>
|
||||
Already have an account? <ULink to="/login" class="text-primary-500 font-medium">Login</ULink>.
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
By signing up, you agree to our <ULink to="/" class="text-primary-500 font-medium">Terms of Service</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
import { signUpSchema, type SignUpSchema } from '~/types/schemas'
|
||||
|
||||
definePageMeta({ layout: 'auth' })
|
||||
|
||||
useSeoMeta({ title: 'Sign up' })
|
||||
|
||||
const toast = useToast()
|
||||
const { signUp } = useAuth()
|
||||
|
||||
const fields = [
|
||||
{
|
||||
name: 'name',
|
||||
type: 'text' as const,
|
||||
label: 'Name',
|
||||
placeholder: 'Enter your name'
|
||||
},
|
||||
{
|
||||
name: 'email',
|
||||
type: 'text' as const,
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email'
|
||||
},
|
||||
{
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password' as const,
|
||||
placeholder: 'Enter your password'
|
||||
}
|
||||
]
|
||||
|
||||
const providers = [
|
||||
{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'Google', description: 'Login with Google' })
|
||||
}
|
||||
},
|
||||
{
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => {
|
||||
toast.add({ title: 'GitHub', description: 'Login with GitHub' })
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
function onSignUpSubmit(payload: FormSubmitEvent<SignUpSchema>) {
|
||||
signUp(payload)
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user