Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion backend/app/routes/households.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
from app.utils.database import (
create_household, get_household, get_user_households,
update_household, delete_household, add_member_to_household,
get_user_by_id
remove_member_from_household, get_user_by_id,
get_household_wrapped_keys, update_household_wrapped_keys
)

router = APIRouter()
Expand Down Expand Up @@ -153,3 +154,34 @@ async def leave_household(household_id: str, user: dict = Depends(get_current_us
update_user(user["user_id"], {"households": households})

return {"message": "Left household successfully"}

@router.delete("/{household_id}/members/{member_id}")
async def remove_household_member(
household_id: str,
member_id: str,
user: dict = Depends(get_current_user)
):
"""Remove a member from household (owner only)"""
household = get_household(household_id)
if not household:
raise HTTPException(status_code=404, detail="Household not found")

if household["owner_id"] != user["user_id"]:
raise HTTPException(status_code=403, detail="Only the owner can remove members")

if member_id == household["owner_id"]:
raise HTTPException(status_code=400, detail="Cannot remove the owner from the household")

if member_id not in household.get("members", []):
raise HTTPException(status_code=404, detail="Member not found in household")

if not remove_member_from_household(household_id, member_id):
raise HTTPException(status_code=500, detail="Failed to remove member")

# Remove member's wrapped encryption key
wrapped_keys = get_household_wrapped_keys(household_id) or {}
if member_id in wrapped_keys:
del wrapped_keys[member_id]
update_household_wrapped_keys(household_id, wrapped_keys)

return {"message": "Member removed successfully"}
29 changes: 29 additions & 0 deletions backend/app/utils/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,35 @@ def delete_household(household_id: str) -> bool:
except ClientError:
return False

def remove_member_from_household(household_id: str, user_id: str) -> bool:
"""Remove a member from household"""
household = get_household(household_id)
if not household:
return False

members = [m for m in household.get("members", []) if m != user_id]

table = get_table(settings.HOUSEHOLDS_TABLE)
try:
table.update_item(
Key={"household_id": household_id},
UpdateExpression="SET members = :m, updated_at = :now",
ExpressionAttributeValues={
":m": members,
":now": datetime.utcnow().isoformat()
}
)

# Remove household from user's list
db_user = get_user_by_id(user_id)
if db_user:
households = [h for h in db_user.get("households", []) if h != household_id]
update_user(user_id, {"households": households})

return True
except ClientError:
return False

# ============================================
# List Operations
# ============================================
Expand Down
16 changes: 0 additions & 16 deletions frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
);
}

function AuthRedirect({ children }: { children: React.ReactNode }) {
const { isAuthenticated, isLoading } = useAuthStore();
if (isLoading) return <LoadingScreen />;
if (isAuthenticated) return <Navigate to="/dashboard" replace />;
return <>{children}</>;
}

function App() {
const { isLoading, setLoading } = useAuthStore();
const { theme } = useThemeStore();
Expand Down Expand Up @@ -70,8 +77,8 @@ function App() {
return (
<BrowserRouter>
<Routes>
{/* Public routes */}
<Route path="/" element={<LandingPage />} />
{/* Public routes - redirect to dashboard if authenticated */}
<Route path="/" element={<AuthRedirect><LandingPage /></AuthRedirect>} />
<Route path="/login" element={<LoginPage />} />
<Route path="/callback" element={<CallbackPage />} />
<Route path="/invite/:inviteId" element={<InvitePage />} />
Expand Down
97 changes: 26 additions & 71 deletions frontend/src/components/EncryptionProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
/**
* EncryptionProvider Component
*
* Wraps protected routes to handle encryption initialization,
* setup prompt, and unlock flow.
* Encryption is optional - the app loads normally while checking encryption status.
* Initializes encryption silently in the background.
* Does NOT block the UI — encryption unlock happens on-demand
* when a user accesses an encrypted note or tries to encrypt one.
*/

import { useEffect, useState } from 'react';
import { useEncryption } from '../hooks/useEncryption';
import EncryptionSetup from './EncryptionSetup';
import { useUnlockModal } from '../hooks/useUnlockModal';
import UnlockPrompt from './UnlockPrompt';

interface EncryptionProviderProps {
Expand All @@ -17,78 +17,33 @@ interface EncryptionProviderProps {

export default function EncryptionProvider({ children }: EncryptionProviderProps) {
const {
isInitialized,
needsSetup,
needsUnlock,
keyData,
initializeEncryption,
keyData,
} = useEncryption();

const [showSetup, setShowSetup] = useState(false);
const [setupSkipped, setSetupSkipped] = useState(false);
const [encryptionChecked, setEncryptionChecked] = useState(false);
const { isOpen, close, resolve } = useUnlockModal();

// Initialize encryption on mount (non-blocking)
// Initialize encryption silently on mount (non-blocking)
useEffect(() => {
const checkEncryption = async () => {
try {
await initializeEncryption();
} catch {
// Encryption check failed - continue without encryption
} finally {
setEncryptionChecked(true);
}
};
checkEncryption();
initializeEncryption().catch(() => {
// Encryption check failed - continue without encryption
});
}, [initializeEncryption]);

// Show setup if needed and not skipped (only after encryption check completes)
useEffect(() => {
if (encryptionChecked && needsSetup && !setupSkipped) {
setShowSetup(true);
}
}, [encryptionChecked, needsSetup, setupSkipped]);

const handleSetupComplete = () => {
setShowSetup(false);
};

const handleSetupSkip = () => {
setSetupSkipped(true);
setShowSetup(false);
};

const handleUnlock = () => {
// Encryption unlocked - continue to app
};

// Wait only for crypto store initialization (synchronous)
if (!isInitialized) {
return null;
}

// Show setup wizard if needed
if (showSetup && needsSetup) {
return (
<EncryptionSetup
onComplete={handleSetupComplete}
onSkip={handleSetupSkip}
/>
);
}

// Show unlock prompt if needed
if (encryptionChecked && needsUnlock && keyData) {
return (
<UnlockPrompt
encryptedPrivateKey={keyData.encryptedPrivateKey}
salt={keyData.salt}
publicKey={keyData.publicKey}
onUnlock={handleUnlock}
/>
);
}

// Render children (the protected content)
return <>{children}</>;
return (
<>
{children}

{/* On-demand unlock modal */}
{isOpen && keyData && (
<UnlockPrompt
encryptedPrivateKey={keyData.encryptedPrivateKey}
salt={keyData.salt}
publicKey={keyData.publicKey}
onUnlock={resolve}
onCancel={close}
/>
)}
</>
);
}
Loading