Skip to content

Commit c745075

Browse files
authored
Merge pull request #20 from voidreamer/staging
Staging
2 parents 22c7054 + 397a395 commit c745075

13 files changed

Lines changed: 486 additions & 105 deletions

File tree

backend/app/routes/households.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from app.utils.database import (
1212
create_household, get_household, get_user_households,
1313
update_household, delete_household, add_member_to_household,
14-
get_user_by_id
14+
remove_member_from_household, get_user_by_id,
15+
get_household_wrapped_keys, update_household_wrapped_keys
1516
)
1617

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

155156
return {"message": "Left household successfully"}
157+
158+
@router.delete("/{household_id}/members/{member_id}")
159+
async def remove_household_member(
160+
household_id: str,
161+
member_id: str,
162+
user: dict = Depends(get_current_user)
163+
):
164+
"""Remove a member from household (owner only)"""
165+
household = get_household(household_id)
166+
if not household:
167+
raise HTTPException(status_code=404, detail="Household not found")
168+
169+
if household["owner_id"] != user["user_id"]:
170+
raise HTTPException(status_code=403, detail="Only the owner can remove members")
171+
172+
if member_id == household["owner_id"]:
173+
raise HTTPException(status_code=400, detail="Cannot remove the owner from the household")
174+
175+
if member_id not in household.get("members", []):
176+
raise HTTPException(status_code=404, detail="Member not found in household")
177+
178+
if not remove_member_from_household(household_id, member_id):
179+
raise HTTPException(status_code=500, detail="Failed to remove member")
180+
181+
# Remove member's wrapped encryption key
182+
wrapped_keys = get_household_wrapped_keys(household_id) or {}
183+
if member_id in wrapped_keys:
184+
del wrapped_keys[member_id]
185+
update_household_wrapped_keys(household_id, wrapped_keys)
186+
187+
return {"message": "Member removed successfully"}

backend/app/utils/database.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,35 @@ def delete_household(household_id: str) -> bool:
204204
except ClientError:
205205
return False
206206

207+
def remove_member_from_household(household_id: str, user_id: str) -> bool:
208+
"""Remove a member from household"""
209+
household = get_household(household_id)
210+
if not household:
211+
return False
212+
213+
members = [m for m in household.get("members", []) if m != user_id]
214+
215+
table = get_table(settings.HOUSEHOLDS_TABLE)
216+
try:
217+
table.update_item(
218+
Key={"household_id": household_id},
219+
UpdateExpression="SET members = :m, updated_at = :now",
220+
ExpressionAttributeValues={
221+
":m": members,
222+
":now": datetime.utcnow().isoformat()
223+
}
224+
)
225+
226+
# Remove household from user's list
227+
db_user = get_user_by_id(user_id)
228+
if db_user:
229+
households = [h for h in db_user.get("households", []) if h != household_id]
230+
update_user(user_id, {"households": households})
231+
232+
return True
233+
except ClientError:
234+
return False
235+
207236
# ============================================
208237
# List Operations
209238
# ============================================

frontend/package-lock.json

Lines changed: 0 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/src/App.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,13 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) {
4242
);
4343
}
4444

45+
function AuthRedirect({ children }: { children: React.ReactNode }) {
46+
const { isAuthenticated, isLoading } = useAuthStore();
47+
if (isLoading) return <LoadingScreen />;
48+
if (isAuthenticated) return <Navigate to="/dashboard" replace />;
49+
return <>{children}</>;
50+
}
51+
4552
function App() {
4653
const { isLoading, setLoading } = useAuthStore();
4754
const { theme } = useThemeStore();
@@ -70,8 +77,8 @@ function App() {
7077
return (
7178
<BrowserRouter>
7279
<Routes>
73-
{/* Public routes */}
74-
<Route path="/" element={<LandingPage />} />
80+
{/* Public routes - redirect to dashboard if authenticated */}
81+
<Route path="/" element={<AuthRedirect><LandingPage /></AuthRedirect>} />
7582
<Route path="/login" element={<LoginPage />} />
7683
<Route path="/callback" element={<CallbackPage />} />
7784
<Route path="/invite/:inviteId" element={<InvitePage />} />
Lines changed: 26 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
/**
22
* EncryptionProvider Component
33
*
4-
* Wraps protected routes to handle encryption initialization,
5-
* setup prompt, and unlock flow.
6-
* Encryption is optional - the app loads normally while checking encryption status.
4+
* Initializes encryption silently in the background.
5+
* Does NOT block the UI — encryption unlock happens on-demand
6+
* when a user accesses an encrypted note or tries to encrypt one.
77
*/
88

99
import { useEffect, useState } from 'react';
1010
import { useEncryption } from '../hooks/useEncryption';
11-
import EncryptionSetup from './EncryptionSetup';
11+
import { useUnlockModal } from '../hooks/useUnlockModal';
1212
import UnlockPrompt from './UnlockPrompt';
1313

1414
interface EncryptionProviderProps {
@@ -17,78 +17,33 @@ interface EncryptionProviderProps {
1717

1818
export default function EncryptionProvider({ children }: EncryptionProviderProps) {
1919
const {
20-
isInitialized,
21-
needsSetup,
22-
needsUnlock,
23-
keyData,
2420
initializeEncryption,
21+
keyData,
2522
} = useEncryption();
2623

27-
const [showSetup, setShowSetup] = useState(false);
28-
const [setupSkipped, setSetupSkipped] = useState(false);
29-
const [encryptionChecked, setEncryptionChecked] = useState(false);
24+
const { isOpen, close, resolve } = useUnlockModal();
3025

31-
// Initialize encryption on mount (non-blocking)
26+
// Initialize encryption silently on mount (non-blocking)
3227
useEffect(() => {
33-
const checkEncryption = async () => {
34-
try {
35-
await initializeEncryption();
36-
} catch {
37-
// Encryption check failed - continue without encryption
38-
} finally {
39-
setEncryptionChecked(true);
40-
}
41-
};
42-
checkEncryption();
28+
initializeEncryption().catch(() => {
29+
// Encryption check failed - continue without encryption
30+
});
4331
}, [initializeEncryption]);
4432

45-
// Show setup if needed and not skipped (only after encryption check completes)
46-
useEffect(() => {
47-
if (encryptionChecked && needsSetup && !setupSkipped) {
48-
setShowSetup(true);
49-
}
50-
}, [encryptionChecked, needsSetup, setupSkipped]);
51-
52-
const handleSetupComplete = () => {
53-
setShowSetup(false);
54-
};
55-
56-
const handleSetupSkip = () => {
57-
setSetupSkipped(true);
58-
setShowSetup(false);
59-
};
60-
61-
const handleUnlock = () => {
62-
// Encryption unlocked - continue to app
63-
};
64-
65-
// Wait only for crypto store initialization (synchronous)
66-
if (!isInitialized) {
67-
return null;
68-
}
69-
70-
// Show setup wizard if needed
71-
if (showSetup && needsSetup) {
72-
return (
73-
<EncryptionSetup
74-
onComplete={handleSetupComplete}
75-
onSkip={handleSetupSkip}
76-
/>
77-
);
78-
}
79-
80-
// Show unlock prompt if needed
81-
if (encryptionChecked && needsUnlock && keyData) {
82-
return (
83-
<UnlockPrompt
84-
encryptedPrivateKey={keyData.encryptedPrivateKey}
85-
salt={keyData.salt}
86-
publicKey={keyData.publicKey}
87-
onUnlock={handleUnlock}
88-
/>
89-
);
90-
}
91-
92-
// Render children (the protected content)
93-
return <>{children}</>;
33+
return (
34+
<>
35+
{children}
36+
37+
{/* On-demand unlock modal */}
38+
{isOpen && keyData && (
39+
<UnlockPrompt
40+
encryptedPrivateKey={keyData.encryptedPrivateKey}
41+
salt={keyData.salt}
42+
publicKey={keyData.publicKey}
43+
onUnlock={resolve}
44+
onCancel={close}
45+
/>
46+
)}
47+
</>
48+
);
9449
}

0 commit comments

Comments
 (0)