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:
@@ -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'
|
||||
|
||||
118
legalconsenthub/composables/notification/useNotification.ts
Normal file
118
legalconsenthub/composables/notification/useNotification.ts
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
58
legalconsenthub/composables/user/useUser.ts
Normal file
58
legalconsenthub/composables/user/useUser.ts
Normal 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
|
||||
}
|
||||
}
|
||||
39
legalconsenthub/composables/user/useUserApi.ts
Normal file
39
legalconsenthub/composables/user/useUserApi.ts
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user