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

@@ -55,17 +55,39 @@ export const auth = betterAuth({
const roleDisplayName = roleDisplayNames[data.role as LegalRole] || data.role
await resend.emails.send({
from: 'Legal Consent Hub <noreply@legalconsenthub.com>',
to: data.email,
subject: `Einladung als ${roleDisplayName} - ${data.organization.name}`,
html: `
<h2>Einladung zur Organisation ${data.organization.name}</h2>
<p>Sie wurden als <strong>${roleDisplayName}</strong> eingeladen.</p>
<p><a href="${inviteLink}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Einladung annehmen</a></p>
<p>Diese Einladung läuft ab am: ${new Date(data.invitation.expiresAt).toLocaleDateString('de-DE')}</p>
`
})
try {
const result = await resend.emails.send({
from: 'Acme <onboarding@resend.dev>',
to: data.email,
subject: `Einladung als ${roleDisplayName} - ${data.organization.name}`,
html: `
<h2>Einladung zur Organisation ${data.organization.name}</h2>
<p>Sie wurden als <strong>${roleDisplayName}</strong> eingeladen.</p>
<p><a href="${inviteLink}" style="background: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;">Einladung annehmen</a></p>
<p>Diese Einladung läuft ab am: ${new Date(data.invitation.expiresAt).toLocaleDateString('de-DE')}</p>
`
})
if (result.error) {
throw new Error(`Email sending failed: ${result.error?.statusCode} ${result.error?.error}`)
}
console.log('Email invite link:', inviteLink)
console.log('Invitation email sent successfully to:', data.email, 'with ID:', result.data?.id)
} catch (error) {
console.error('Failed to send invitation email:', error)
// Log specific error details for debugging
const errorObj = error as { response?: { status: number; data: unknown }; message?: string }
if (errorObj.response) {
console.error('HTTP Status:', errorObj.response.status)
console.error('Response data:', errorObj.response.data)
}
// Re-throw the error so BetterAuth knows the email failed
const message = errorObj.message || String(error)
throw new Error(`Email sending failed: ${message}`)
}
}
})
]

View File

@@ -1,55 +1,87 @@
import { createAccessControl } from 'better-auth/plugins/access'
import { defaultStatements, adminAc, memberAc, ownerAc } from 'better-auth/plugins/organization/access'
import { defu } from 'defu'
export const statement = {
const customStatements = {
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
} as const
export const statement = {
...customStatements,
...defaultStatements
} as const
export const accessControl = createAccessControl(statement)
// Roles with specific permissions
export const employerRole = accessControl.newRole({
application_form: ['create', 'read', 'approve', 'reject'],
agreement: ['create', 'read', 'sign', 'approve'],
member: ['invite', 'read'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
})
export const employerRole = accessControl.newRole(
defu(
{
application_form: ['create', 'read', 'approve', 'reject'],
agreement: ['create', 'read', 'sign', 'approve'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
},
memberAc.statements
) as Parameters<typeof accessControl.newRole>[0]
)
export const worksCouncilMemberRole = accessControl.newRole({
application_form: ['create', 'read', 'update', 'submit'],
agreement: ['read', 'sign', 'approve'],
member: ['read'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'download', 'upload']
})
export const worksCouncilMemberRole = accessControl.newRole(
defu(
{
application_form: ['create', 'read', 'update', 'submit'],
agreement: ['read', 'sign', 'approve'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'download', 'upload']
},
memberAc.statements
) as Parameters<typeof accessControl.newRole>[0]
)
export const employeeRole = accessControl.newRole({
application_form: ['read'],
agreement: ['read'],
member: ['read'],
comment: ['create', 'read'],
document: ['read', 'download']
})
export const employeeRole = accessControl.newRole(
defu(
{
application_form: ['read'],
agreement: ['read'],
comment: ['create', 'read'],
document: ['read', 'download']
},
memberAc.statements
) as Parameters<typeof accessControl.newRole>[0]
)
export const adminRole = accessControl.newRole({
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject'],
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
organization: ['create', 'read', 'update', 'delete', 'manage_settings'],
member: ['create', 'read', 'update', 'delete', 'invite', 'remove'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
})
export const adminRole = accessControl.newRole(
defu(
{
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject'],
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
},
adminAc.statements
) as Parameters<typeof accessControl.newRole>[0]
)
export const ownerRole = accessControl.newRole(
defu(
{
application_form: ['create', 'read', 'update', 'delete', 'approve', 'reject', 'submit'],
agreement: ['create', 'read', 'update', 'sign', 'approve', 'reject'],
comment: ['create', 'read', 'update', 'delete'],
document: ['create', 'read', 'update', 'delete', 'download', 'upload']
},
ownerAc.statements
) as Parameters<typeof accessControl.newRole>[0]
)
export const ROLES = {
EMPLOYER: 'employer',
WORKS_COUNCIL_MEMBER: 'works_council_member',
EMPLOYEE: 'employee',
ADMIN: 'admin'
ADMIN: 'admin',
OWNER: 'owner'
} as const
export type LegalRole = (typeof ROLES)[keyof typeof ROLES]