From 34dcbb74fed0f9272c7c57b9eb6aeddc57738fc3 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 30 Mar 2026 11:24:43 +0200 Subject: [PATCH 1/2] fix: resolve merge conflicts from role-based permissions + audit trail branches Combines auth context keys (user role, IP, user-agent), tenant resolver (GetUserRole-based access verification), middleware (deferred tenant resolution + request info capture), and router (audit log + notifications + assignments). --- backend/internal/auth/context.go | 32 +++++----------- backend/internal/auth/middleware.go | 33 +--------------- backend/internal/auth/tenant_resolver.go | 38 +++++++------------ backend/internal/auth/tenant_resolver_test.go | 31 ++------------- backend/internal/handlers/deadlines.go | 12 +----- backend/internal/router/router.go | 21 +--------- 6 files changed, 32 insertions(+), 135 deletions(-) diff --git a/backend/internal/auth/context.go b/backend/internal/auth/context.go index c2aeaef..9553104 100644 --- a/backend/internal/auth/context.go +++ b/backend/internal/auth/context.go @@ -9,19 +9,11 @@ import ( type contextKey string const ( -<<<<<<< HEAD userIDKey contextKey = "user_id" tenantIDKey contextKey = "tenant_id" + userRoleKey contextKey = "user_role" ipKey contextKey = "ip_address" userAgentKey contextKey = "user_agent" -||||||| 82878df - userIDKey contextKey = "user_id" - tenantIDKey contextKey = "tenant_id" -======= - userIDKey contextKey = "user_id" - tenantIDKey contextKey = "tenant_id" - userRoleKey contextKey = "user_role" ->>>>>>> mai/pike/p0-role-based ) func ContextWithUserID(ctx context.Context, userID uuid.UUID) context.Context { @@ -41,7 +33,15 @@ func TenantFromContext(ctx context.Context) (uuid.UUID, bool) { id, ok := ctx.Value(tenantIDKey).(uuid.UUID) return id, ok } -<<<<<<< HEAD + +func ContextWithUserRole(ctx context.Context, role string) context.Context { + return context.WithValue(ctx, userRoleKey, role) +} + +func UserRoleFromContext(ctx context.Context) string { + role, _ := ctx.Value(userRoleKey).(string) + return role +} func ContextWithRequestInfo(ctx context.Context, ip, userAgent string) context.Context { ctx = context.WithValue(ctx, ipKey, ip) @@ -62,15 +62,3 @@ func UserAgentFromContext(ctx context.Context) *string { } return nil } -||||||| 82878df -======= - -func ContextWithUserRole(ctx context.Context, role string) context.Context { - return context.WithValue(ctx, userRoleKey, role) -} - -func UserRoleFromContext(ctx context.Context) string { - role, _ := ctx.Value(userRoleKey).(string) - return role -} ->>>>>>> mai/pike/p0-role-based diff --git a/backend/internal/auth/middleware.go b/backend/internal/auth/middleware.go index 02428c6..1a51b11 100644 --- a/backend/internal/auth/middleware.go +++ b/backend/internal/auth/middleware.go @@ -35,36 +35,6 @@ func (m *Middleware) RequireAuth(next http.Handler) http.Handler { } ctx := ContextWithUserID(r.Context(), userID) -<<<<<<< HEAD - // Tenant resolution is handled by TenantResolver middleware for scoped routes. - // Tenant management routes handle their own access control. -||||||| 82878df - - // Resolve tenant and role from user_tenants - var membership struct { - TenantID uuid.UUID `db:"tenant_id"` - Role string `db:"role"` - } - err = m.db.GetContext(r.Context(), &membership, - "SELECT tenant_id, role FROM user_tenants WHERE user_id = $1 LIMIT 1", userID) - if err != nil { - http.Error(w, "no tenant found for user", http.StatusForbidden) - return - } - ctx = ContextWithTenantID(ctx, membership.TenantID) - ctx = ContextWithUserRole(ctx, membership.Role) - -======= - - // Resolve tenant from user_tenants - var tenantID uuid.UUID - err = m.db.GetContext(r.Context(), &tenantID, - "SELECT tenant_id FROM user_tenants WHERE user_id = $1 LIMIT 1", userID) - if err != nil { - http.Error(w, "no tenant found for user", http.StatusForbidden) - return - } - ctx = ContextWithTenantID(ctx, tenantID) // Capture IP and user-agent for audit logging ip := r.Header.Get("X-Forwarded-For") @@ -73,7 +43,8 @@ func (m *Middleware) RequireAuth(next http.Handler) http.Handler { } ctx = ContextWithRequestInfo(ctx, ip, r.UserAgent()) ->>>>>>> mai/knuth/p0-audit-trail-append + // Tenant/role resolution is handled by TenantResolver middleware for scoped routes. + // Tenant management routes handle their own access control. next.ServeHTTP(w, r.WithContext(ctx)) }) } diff --git a/backend/internal/auth/tenant_resolver.go b/backend/internal/auth/tenant_resolver.go index 0dab57f..811f872 100644 --- a/backend/internal/auth/tenant_resolver.go +++ b/backend/internal/auth/tenant_resolver.go @@ -12,12 +12,7 @@ import ( // Defined as an interface to avoid circular dependency with services. type TenantLookup interface { FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) -<<<<<<< HEAD - VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) -||||||| 82878df -======= GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) ->>>>>>> mai/pike/p0-role-based } // TenantResolver is middleware that resolves the tenant from X-Tenant-ID header @@ -46,38 +41,22 @@ func (tr *TenantResolver) Resolve(next http.Handler) http.Handler { http.Error(w, `{"error":"invalid X-Tenant-ID"}`, http.StatusBadRequest) return } -<<<<<<< HEAD - - // Verify user has access to this tenant - hasAccess, err := tr.lookup.VerifyAccess(r.Context(), userID, parsed) + // Verify user has access and get their role + role, err := tr.lookup.GetUserRole(r.Context(), userID, parsed) if err != nil { slog.Error("tenant access check failed", "error", err, "user_id", userID, "tenant_id", parsed) http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) return } - if !hasAccess { + if role == "" { http.Error(w, `{"error":"no access to tenant"}`, http.StatusForbidden) return } -||||||| 82878df -======= - // Verify user has access and get their role - role, err := tr.lookup.GetUserRole(r.Context(), userID, parsed) - if err != nil { - http.Error(w, "error checking tenant access", http.StatusInternalServerError) - return - } - if role == "" { - http.Error(w, "no access to this tenant", http.StatusForbidden) - return - } ->>>>>>> mai/pike/p0-role-based tenantID = parsed - // Override the role from middleware with the correct one for this tenant r = r.WithContext(ContextWithUserRole(r.Context(), role)) } else { - // Default to user's first tenant (role already set by middleware) + // Default to user's first tenant first, err := tr.lookup.FirstTenantForUser(r.Context(), userID) if err != nil { slog.Error("failed to resolve default tenant", "error", err, "user_id", userID) @@ -89,6 +68,15 @@ func (tr *TenantResolver) Resolve(next http.Handler) http.Handler { return } tenantID = *first + + // Resolve role for default tenant + role, err := tr.lookup.GetUserRole(r.Context(), userID, tenantID) + if err != nil { + slog.Error("failed to resolve role for default tenant", "error", err) + http.Error(w, `{"error":"internal error"}`, http.StatusInternalServerError) + return + } + r = r.WithContext(ContextWithUserRole(r.Context(), role)) } ctx := ContextWithTenantID(r.Context(), tenantID) diff --git a/backend/internal/auth/tenant_resolver_test.go b/backend/internal/auth/tenant_resolver_test.go index d0300c2..c35d1f2 100644 --- a/backend/internal/auth/tenant_resolver_test.go +++ b/backend/internal/auth/tenant_resolver_test.go @@ -10,49 +10,26 @@ import ( ) type mockTenantLookup struct { -<<<<<<< HEAD - tenantID *uuid.UUID - err error - hasAccess bool - accessErr error -||||||| 82878df - tenantID *uuid.UUID - err error -======= tenantID *uuid.UUID role string + roleSet bool // true means role was explicitly set (even if empty) err error ->>>>>>> mai/pike/p0-role-based } func (m *mockTenantLookup) FirstTenantForUser(ctx context.Context, userID uuid.UUID) (*uuid.UUID, error) { return m.tenantID, m.err } -<<<<<<< HEAD -func (m *mockTenantLookup) VerifyAccess(ctx context.Context, userID, tenantID uuid.UUID) (bool, error) { - return m.hasAccess, m.accessErr -} - -||||||| 82878df -======= func (m *mockTenantLookup) GetUserRole(ctx context.Context, userID, tenantID uuid.UUID) (string, error) { - if m.role != "" { + if m.roleSet { return m.role, m.err } return "associate", m.err } ->>>>>>> mai/pike/p0-role-based func TestTenantResolver_FromHeader(t *testing.T) { tenantID := uuid.New() -<<<<<<< HEAD - tr := NewTenantResolver(&mockTenantLookup{hasAccess: true}) -||||||| 82878df - tr := NewTenantResolver(&mockTenantLookup{}) -======= tr := NewTenantResolver(&mockTenantLookup{role: "partner"}) ->>>>>>> mai/pike/p0-role-based var gotTenantID uuid.UUID next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -81,7 +58,7 @@ func TestTenantResolver_FromHeader(t *testing.T) { func TestTenantResolver_FromHeader_NoAccess(t *testing.T) { tenantID := uuid.New() - tr := NewTenantResolver(&mockTenantLookup{hasAccess: false}) + tr := NewTenantResolver(&mockTenantLookup{role: "", roleSet: true}) next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Fatal("next should not be called") @@ -101,7 +78,7 @@ func TestTenantResolver_FromHeader_NoAccess(t *testing.T) { func TestTenantResolver_DefaultsToFirst(t *testing.T) { tenantID := uuid.New() - tr := NewTenantResolver(&mockTenantLookup{tenantID: &tenantID}) + tr := NewTenantResolver(&mockTenantLookup{tenantID: &tenantID, role: "owner"}) var gotTenantID uuid.UUID next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/backend/internal/handlers/deadlines.go b/backend/internal/handlers/deadlines.go index e1a2a39..1ff7629 100644 --- a/backend/internal/handlers/deadlines.go +++ b/backend/internal/handlers/deadlines.go @@ -198,18 +198,8 @@ func (h *DeadlineHandlers) Delete(w http.ResponseWriter, r *http.Request) { return } -<<<<<<< HEAD - if err := h.deadlines.Delete(tenantID, deadlineID); err != nil { + if err := h.deadlines.Delete(r.Context(), tenantID, deadlineID); err != nil { writeError(w, http.StatusNotFound, "deadline not found") -||||||| 82878df - err = h.deadlines.Delete(tenantID, deadlineID) - if err != nil { - writeError(w, http.StatusNotFound, err.Error()) -======= - err = h.deadlines.Delete(r.Context(), tenantID, deadlineID) - if err != nil { - writeError(w, http.StatusNotFound, err.Error()) ->>>>>>> mai/knuth/p0-audit-trail-append return } diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 84b7f8e..cd4b471 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -29,14 +29,8 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se deadlineRuleSvc := services.NewDeadlineRuleService(db) calculator := services.NewDeadlineCalculator(holidaySvc) storageCli := services.NewStorageClient(cfg.SupabaseURL, cfg.SupabaseServiceKey) -<<<<<<< HEAD documentSvc := services.NewDocumentService(db, storageCli, auditSvc) -||||||| 82878df - documentSvc := services.NewDocumentService(db, storageCli) -======= - documentSvc := services.NewDocumentService(db, storageCli) assignmentSvc := services.NewCaseAssignmentService(db) ->>>>>>> mai/pike/p0-role-based // AI service (optional — only if API key is configured) var aiH *handlers.AIHandler @@ -79,6 +73,7 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se api := http.NewServeMux() // Tenant management (no tenant resolver — these operate across tenants) + api.HandleFunc("POST /api/tenants/auto-assign", tenantH.AutoAssign) api.HandleFunc("POST /api/tenants", tenantH.CreateTenant) api.HandleFunc("GET /api/tenants", tenantH.ListTenants) api.HandleFunc("GET /api/tenants/{id}", tenantH.GetTenant) @@ -162,16 +157,10 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se // Dashboard — all can view scoped.HandleFunc("GET /api/dashboard", dashboardH.Get) -<<<<<<< HEAD // Audit log - scoped.HandleFunc("GET /api/audit-log", auditH.List) + scoped.HandleFunc("GET /api/audit-log", perm(auth.PermViewAuditLog, auditH.List)) - // Documents -||||||| 82878df - // Documents -======= // Documents — all can upload, delete checked in handler (own vs all) ->>>>>>> mai/pike/p0-role-based scoped.HandleFunc("GET /api/cases/{id}/documents", docH.ListByCase) scoped.HandleFunc("POST /api/cases/{id}/documents", perm(auth.PermUploadDocuments, docH.Upload)) scoped.HandleFunc("GET /api/documents/{docId}", docH.Download) @@ -185,7 +174,6 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se scoped.HandleFunc("POST /api/ai/summarize-case", perm(auth.PermAIExtraction, aiLimiter.LimitFunc(aiH.SummarizeCase))) } -<<<<<<< HEAD // Notifications if notifH != nil { scoped.HandleFunc("GET /api/notifications", notifH.List) @@ -196,12 +184,7 @@ func New(db *sqlx.DB, authMW *auth.Middleware, cfg *config.Config, calDAVSvc *se scoped.HandleFunc("PUT /api/notification-preferences", notifH.UpdatePreferences) } - // CalDAV sync endpoints -||||||| 82878df - // CalDAV sync endpoints -======= // CalDAV sync endpoints — settings permission required ->>>>>>> mai/pike/p0-role-based if calDAVSvc != nil { calDAVH := handlers.NewCalDAVHandler(calDAVSvc) scoped.HandleFunc("POST /api/caldav/sync", perm(auth.PermManageSettings, calDAVH.TriggerSync)) From 118bae1ae30ef73f60a84965bfc3db873e4fd0d3 Mon Sep 17 00:00:00 2001 From: m Date: Mon, 30 Mar 2026 11:24:52 +0200 Subject: [PATCH 2/2] feat: HL tenant setup + email domain auto-assignment - Create pre-configured Hogan Lovells tenant with demo flag and auto_assign_domains: ["hoganlovells.com"] - Add POST /api/tenants/auto-assign endpoint: checks email domain against tenant settings, auto-assigns user as associate if match - Add AutoAssignByDomain to TenantService - Update registration flow: after signup, check auto-assign before showing tenant creation form. Skip tenant creation if auto-assigned. - Add DemoBanner component shown when tenant.settings.demo is true - Extend GET /api/me to return is_demo flag from tenant settings --- backend/internal/handlers/tenant_handler.go | 80 +++++++++++++++++++ backend/internal/services/tenant_service.go | 48 +++++++++++ frontend/src/app/(app)/layout.tsx | 2 + frontend/src/app/(auth)/register/page.tsx | 72 ++++++++++++----- frontend/src/components/layout/DemoBanner.tsx | 17 ++++ frontend/src/lib/hooks/usePermissions.ts | 1 + frontend/src/lib/types.ts | 1 + 7 files changed, 203 insertions(+), 18 deletions(-) create mode 100644 frontend/src/components/layout/DemoBanner.tsx diff --git a/backend/internal/handlers/tenant_handler.go b/backend/internal/handlers/tenant_handler.go index 6341b1d..8c77ac3 100644 --- a/backend/internal/handlers/tenant_handler.go +++ b/backend/internal/handlers/tenant_handler.go @@ -356,6 +356,71 @@ func (h *TenantHandler) UpdateMemberRole(w http.ResponseWriter, r *http.Request) jsonResponse(w, map[string]string{"status": "updated"}, http.StatusOK) } +// AutoAssign handles POST /api/tenants/auto-assign — checks if the user's email domain +// matches any tenant's auto_assign_domains and assigns them if so. +func (h *TenantHandler) AutoAssign(w http.ResponseWriter, r *http.Request) { + userID, ok := auth.UserFromContext(r.Context()) + if !ok { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + + var req struct { + Email string `json:"email"` + } + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + jsonError(w, "invalid request body", http.StatusBadRequest) + return + } + if req.Email == "" { + jsonError(w, "email is required", http.StatusBadRequest) + return + } + + // Extract domain from email + parts := splitEmail(req.Email) + if parts == "" { + jsonError(w, "invalid email format", http.StatusBadRequest) + return + } + + result, err := h.svc.AutoAssignByDomain(r.Context(), userID, parts) + if err != nil { + slog.Error("auto-assign failed", "error", err) + jsonError(w, "internal error", http.StatusInternalServerError) + return + } + + if result == nil { + jsonResponse(w, map[string]any{"assigned": false}, http.StatusOK) + return + } + + jsonResponse(w, map[string]any{ + "assigned": true, + "tenant_id": result.ID, + "name": result.Name, + "slug": result.Slug, + "role": result.Role, + "settings": result.Settings, + }, http.StatusOK) +} + +// splitEmail extracts the domain part from an email address. +func splitEmail(email string) string { + at := -1 + for i, c := range email { + if c == '@' { + at = i + break + } + } + if at < 0 || at >= len(email)-1 { + return "" + } + return email[at+1:] +} + // GetMe handles GET /api/me — returns the current user's ID and role in the active tenant. func (h *TenantHandler) GetMe(w http.ResponseWriter, r *http.Request) { userID, ok := auth.UserFromContext(r.Context()) @@ -370,11 +435,26 @@ func (h *TenantHandler) GetMe(w http.ResponseWriter, r *http.Request) { // Get user's permissions for frontend UI perms := auth.GetRolePermissions(role) + // Check if tenant is in demo mode + isDemo := false + if tenant, err := h.svc.GetByID(r.Context(), tenantID); err == nil && tenant != nil { + var settings map[string]json.RawMessage + if json.Unmarshal(tenant.Settings, &settings) == nil { + if demoRaw, ok := settings["demo"]; ok { + var demo bool + if json.Unmarshal(demoRaw, &demo) == nil { + isDemo = demo + } + } + } + } + jsonResponse(w, map[string]any{ "user_id": userID, "tenant_id": tenantID, "role": role, "permissions": perms, + "is_demo": isDemo, }, http.StatusOK) } diff --git a/backend/internal/services/tenant_service.go b/backend/internal/services/tenant_service.go index e0f5cea..e3eea33 100644 --- a/backend/internal/services/tenant_service.go +++ b/backend/internal/services/tenant_service.go @@ -240,6 +240,54 @@ func (s *TenantService) UpdateMemberRole(ctx context.Context, tenantID, userID u return nil } +// AutoAssignByDomain finds a tenant with a matching auto_assign_domains setting +// and adds the user as a member. Returns the tenant and role, or nil if no match. +func (s *TenantService) AutoAssignByDomain(ctx context.Context, userID uuid.UUID, emailDomain string) (*models.TenantWithRole, error) { + // Find tenant where settings.auto_assign_domains contains this domain + var tenant models.Tenant + err := s.db.GetContext(ctx, &tenant, + `SELECT id, name, slug, settings, created_at, updated_at + FROM tenants + WHERE settings->'auto_assign_domains' ? $1 + LIMIT 1`, + emailDomain, + ) + if err != nil { + return nil, nil // no match — not an error + } + + // Check if already a member + var exists bool + err = s.db.GetContext(ctx, &exists, + `SELECT EXISTS(SELECT 1 FROM user_tenants WHERE user_id = $1 AND tenant_id = $2)`, + userID, tenant.ID, + ) + if err != nil { + return nil, fmt.Errorf("check membership: %w", err) + } + if exists { + // Already a member — return the existing membership + role, err := s.GetUserRole(ctx, userID, tenant.ID) + if err != nil { + return nil, fmt.Errorf("get existing role: %w", err) + } + return &models.TenantWithRole{Tenant: tenant, Role: role}, nil + } + + // Add as member (associate by default for auto-assigned users) + role := "associate" + _, err = s.db.ExecContext(ctx, + `INSERT INTO user_tenants (user_id, tenant_id, role) VALUES ($1, $2, $3)`, + userID, tenant.ID, role, + ) + if err != nil { + return nil, fmt.Errorf("auto-assign user: %w", err) + } + + s.audit.Log(ctx, "create", "auto_membership", &tenant.ID, map[string]any{"domain": emailDomain}, map[string]any{"user_id": userID, "role": role}) + return &models.TenantWithRole{Tenant: tenant, Role: role}, nil +} + // RemoveMember removes a user from a tenant. Cannot remove the last owner. func (s *TenantService) RemoveMember(ctx context.Context, tenantID, userID uuid.UUID) error { // Check if the user being removed is an owner diff --git a/frontend/src/app/(app)/layout.tsx b/frontend/src/app/(app)/layout.tsx index 8740d7d..b102bd9 100644 --- a/frontend/src/app/(app)/layout.tsx +++ b/frontend/src/app/(app)/layout.tsx @@ -1,5 +1,6 @@ import { Sidebar } from "@/components/layout/Sidebar"; import { Header } from "@/components/layout/Header"; +import { DemoBanner } from "@/components/layout/DemoBanner"; export const dynamic = "force-dynamic"; @@ -12,6 +13,7 @@ export default function AppLayout({
+
{children}
diff --git a/frontend/src/app/(auth)/register/page.tsx b/frontend/src/app/(auth)/register/page.tsx index afa4a0a..6bd691d 100644 --- a/frontend/src/app/(auth)/register/page.tsx +++ b/frontend/src/app/(auth)/register/page.tsx @@ -5,12 +5,22 @@ import { api } from "@/lib/api"; import { useRouter } from "next/navigation"; import { useState } from "react"; +interface AutoAssignResponse { + assigned: boolean; + tenant_id?: string; + name?: string; + slug?: string; + role?: string; + settings?: Record; +} + export default function RegisterPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [firmName, setFirmName] = useState(""); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + const [showFirmName, setShowFirmName] = useState(true); const router = useRouter(); const supabase = createClient(); @@ -34,8 +44,30 @@ export default function RegisterPage() { return; } - // 2. Create tenant via backend (the backend adds the user as owner) if (data.session) { + // 2. Check if email domain matches an existing tenant for auto-assignment + try { + const result = await api.post("/tenants/auto-assign", { email }); + if (result.assigned && result.tenant_id) { + // Auto-assigned — store tenant and go to dashboard + localStorage.setItem("kanzlai_tenant_id", result.tenant_id); + router.push("/"); + router.refresh(); + return; + } + } catch { + // Auto-assign failed — fall through to manual tenant creation + } + + // 3. No auto-assignment — create tenant manually + if (!firmName) { + // Show firm name field if not yet visible + setShowFirmName(true); + setError("Bitte geben Sie einen Kanzleinamen ein"); + setLoading(false); + return; + } + try { await api.post("/tenants", { name: firmName }); } catch (err: unknown) { @@ -68,23 +100,27 @@ export default function RegisterPage() {
-
- - setFirmName(e.target.value)} - required - className="mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm placeholder-neutral-400 focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900" - placeholder="Muster & Partner Rechtsanwaelte" - /> -
+ {showFirmName && ( +
+ + setFirmName(e.target.value)} + className="mt-1 block w-full rounded-md border border-neutral-300 px-3 py-2 text-sm placeholder-neutral-400 focus:border-neutral-900 focus:outline-none focus:ring-1 focus:ring-neutral-900" + placeholder="Muster & Partner Rechtsanwaelte" + /> +

+ Leer lassen, falls Sie zu einer bestehenden Kanzlei eingeladen wurden +

+
+ )}
+ ); +} diff --git a/frontend/src/lib/hooks/usePermissions.ts b/frontend/src/lib/hooks/usePermissions.ts index 57e43fe..978dea6 100644 --- a/frontend/src/lib/hooks/usePermissions.ts +++ b/frontend/src/lib/hooks/usePermissions.ts @@ -25,5 +25,6 @@ export function usePermissions() { isLoading, userId: data?.user_id ?? null, tenantId: data?.tenant_id ?? null, + isDemo: data?.is_demo ?? false, }; } diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index a842be4..e011b9f 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -202,6 +202,7 @@ export interface UserInfo { tenant_id: string; role: UserRole; permissions: string[]; + is_demo: boolean; } export type UserRole = "owner" | "partner" | "associate" | "paralegal" | "secretary";