feat(fullstack): Add notifications, user is now an entity, add testcontainers, rework custom permissions, get user from JWT in endpoints

This commit is contained in:
2025-08-09 10:09:00 +02:00
parent a5eae07eaf
commit 7e55a336f2
44 changed files with 1571 additions and 139 deletions

View File

@@ -1,2 +1,6 @@
export { useApplicationFormTemplate } from './applicationFormTemplate/useApplicationFormTemplate'
export { useApplicationForm } from './applicationForm/useApplicationForm'
export { useNotification } from './notification/useNotification'
export { useNotificationApi } from './notification/useNotificationApi'
export { useUser } from './user/useUser'
export { useUserApi } from './user/useUserApi'

View File

@@ -0,0 +1,118 @@
import type { NotificationDto } from '~/.api-client'
export const useNotification = () => {
const {
getNotifications,
getUnreadNotifications,
getUnreadNotificationCount,
markAllNotificationsAsRead,
markNotificationAsRead
} = useNotificationApi()
const notifications = ref<NotificationDto[]>([])
const unreadNotifications = ref<NotificationDto[]>([])
const unreadCount = ref<number>(0)
const isLoading = ref(false)
const fetchNotifications = async (page: number = 0, size: number = 20) => {
isLoading.value = true
try {
const response = await getNotifications(page, size)
notifications.value = response.content || []
return response
} catch (error) {
console.error('Failed to fetch notifications:', error)
throw error
} finally {
isLoading.value = false
}
}
const fetchUnreadNotifications = async () => {
try {
const response = await getUnreadNotifications()
unreadNotifications.value = response || []
return response
} catch (error) {
console.error('Failed to fetch unread notifications:', error)
throw error
}
}
const fetchUnreadCount = async () => {
try {
const count = await getUnreadNotificationCount()
unreadCount.value = count || 0
return count
} catch (error) {
console.error('Failed to fetch unread count:', error)
throw error
}
}
const markAllAsRead = async () => {
try {
await markAllNotificationsAsRead()
unreadCount.value = 0
unreadNotifications.value = []
notifications.value = notifications.value.map((n) => ({ ...n, isRead: true }))
} catch (error) {
console.error('Failed to mark all as read:', error)
throw error
}
}
const markAsRead = async (notificationId: string) => {
try {
await markNotificationAsRead(notificationId)
const index = notifications.value.findIndex((n) => n.id === notificationId)
if (index !== -1) {
notifications.value[index].isRead = true
}
// Remove from unread notifications
unreadNotifications.value = unreadNotifications.value.filter((n) => n.id !== notificationId)
if (unreadCount.value > 0) {
unreadCount.value--
}
} catch (error) {
console.error('Failed to mark notification as read:', error)
throw error
}
}
const handleNotificationClick = async (notification: NotificationDto) => {
if (!notification.isRead) {
await markAsRead(notification.id)
}
if (notification.clickTarget) {
await navigateTo(notification.clickTarget)
}
}
const startPeriodicRefresh = (intervalMs: number = 30000) => {
const interval = setInterval(() => {
fetchUnreadCount()
}, intervalMs)
onUnmounted(() => {
clearInterval(interval)
})
return interval
}
return {
notifications: readonly(notifications),
unreadNotifications: readonly(unreadNotifications),
unreadCount: readonly(unreadCount),
isLoading: readonly(isLoading),
fetchNotifications,
fetchUnreadNotifications,
fetchUnreadCount,
markAllAsRead,
markAsRead,
handleNotificationClick,
startPeriodicRefresh
}
}

View File

@@ -0,0 +1,55 @@
import {
NotificationApi,
Configuration,
type NotificationDto,
type PagedNotificationDto,
type CreateNotificationDto
} from '~/.api-client'
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
export function useNotificationApi() {
const appBaseUrl = useRuntimeConfig().app.baseURL
const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
const { jwt } = useAuth()
const basePath = withoutTrailingSlash(
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
)
const notificationApiClient = new NotificationApi(
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
)
async function createNotification(createNotificationDto: CreateNotificationDto): Promise<NotificationDto> {
return notificationApiClient.createNotification({ createNotificationDto })
}
async function getNotifications(page?: number, size?: number): Promise<PagedNotificationDto> {
return notificationApiClient.getNotifications({ page, size })
}
async function getUnreadNotifications(): Promise<NotificationDto[]> {
return notificationApiClient.getUnreadNotifications()
}
async function getUnreadNotificationCount(): Promise<number> {
return notificationApiClient.getUnreadNotificationCount()
}
async function markAllNotificationsAsRead(): Promise<void> {
return notificationApiClient.markAllNotificationsAsRead()
}
async function markNotificationAsRead(id: string): Promise<NotificationDto> {
return notificationApiClient.markNotificationAsRead({ id })
}
return {
createNotification,
getNotifications,
getUnreadNotifications,
getUnreadNotificationCount,
markAllNotificationsAsRead,
markNotificationAsRead
}
}

View File

@@ -13,6 +13,7 @@ import {
worksCouncilMemberRole,
employeeRole,
adminRole,
ownerRole,
ROLES
} from '~/server/utils/permissions'
@@ -57,7 +58,8 @@ export function useAuth() {
[ROLES.EMPLOYER]: employerRole,
[ROLES.WORKS_COUNCIL_MEMBER]: worksCouncilMemberRole,
[ROLES.EMPLOYEE]: employeeRole,
[ROLES.ADMIN]: adminRole
[ROLES.ADMIN]: adminRole,
[ROLES.OWNER]: ownerRole
}
}),
jwtClient()

View File

@@ -30,11 +30,11 @@ export function usePermissions() {
)
const canInviteMembers = computed(() =>
hasPermission({ member: ["invite"] })
hasPermission({ invitation: ["create"] })
)
const canManageOrganization = computed(() =>
hasPermission({ organization: ["manage_settings"] })
hasPermission({ organization: ["update"] })
)
// Role checks
@@ -42,6 +42,7 @@ export function usePermissions() {
const isEmployee = computed(() => currentRole.value === ROLES.EMPLOYEE)
const isWorksCouncilMember = computed(() => currentRole.value === ROLES.WORKS_COUNCIL_MEMBER)
const isAdmin = computed(() => currentRole.value === ROLES.ADMIN)
const isOwner = computed(() => currentRole.value === ROLES.OWNER)
const getCurrentRoleInfo = () => {
const roleInfo = {
@@ -68,6 +69,12 @@ export function usePermissions() {
description: 'Vollzugriff auf Organisationsverwaltung',
color: 'red',
icon: 'i-lucide-settings'
},
[ROLES.OWNER]: {
name: 'Eigentümer',
description: 'Vollzugriff und Organisationsbesitz',
color: 'yellow',
icon: 'i-lucide-crown'
}
}
@@ -87,6 +94,7 @@ export function usePermissions() {
isEmployee,
isWorksCouncilMember,
isAdmin,
isOwner,
// Computed permissions
canCreateApplicationForm,

View File

@@ -0,0 +1,58 @@
import {
type CreateUserDto,
type UserDto,
ResponseError
} from '~/.api-client'
import { useUserApi } from './useUserApi'
export function useUser() {
const userApi = useUserApi()
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
try {
return await userApi.createUser(createUserDto)
} catch (e: unknown) {
if (e instanceof ResponseError) {
console.error('Failed creating user:', e.response)
} else {
console.error('Failed creating user:', e)
}
return Promise.reject(e)
}
}
async function getUserById(id: string): Promise<UserDto | null> {
try {
return await userApi.getUserById(id)
} catch (e: unknown) {
if (e instanceof ResponseError && e.response.status === 404) {
return null
}
if (e instanceof ResponseError) {
console.error(`Failed retrieving user with ID ${id}:`, e.response)
} else {
console.error(`Failed retrieving user with ID ${id}:`, e)
}
return Promise.reject(e)
}
}
async function deleteUser(id: string): Promise<void> {
try {
return await userApi.deleteUser(id)
} catch (e: unknown) {
if (e instanceof ResponseError) {
console.error(`Failed deleting user with ID ${id}:`, e.response)
} else {
console.error(`Failed deleting user with ID ${id}:`, e)
}
return Promise.reject(e)
}
}
return {
createUser,
getUserById,
deleteUser
}
}

View File

@@ -0,0 +1,39 @@
import {
UserApi,
Configuration,
type CreateUserDto,
type UserDto
} from '~/.api-client'
import { cleanDoubleSlashes, withoutTrailingSlash } from 'ufo'
export function useUserApi() {
const appBaseUrl = useRuntimeConfig().app.baseURL
const { serverApiBaseUrl, serverApiBasePath, clientProxyBasePath } = useRuntimeConfig().public
const { jwt } = useAuth()
const basePath = withoutTrailingSlash(
cleanDoubleSlashes(import.meta.client ? appBaseUrl + clientProxyBasePath : serverApiBaseUrl + serverApiBasePath)
)
const userApiClient = new UserApi(
new Configuration({ basePath, headers: { Authorization: jwt.value ? `Bearer ${jwt.value}` : '' } })
)
async function createUser(createUserDto: CreateUserDto): Promise<UserDto> {
return userApiClient.createUser({ createUserDto })
}
async function getUserById(id: string): Promise<UserDto> {
return userApiClient.getUserById({ id })
}
async function deleteUser(id: string): Promise<void> {
return userApiClient.deleteUser({ id })
}
return {
createUser,
getUserById,
deleteUser
}
}