Skip to content
Open
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
9 changes: 8 additions & 1 deletion frontend/app/api/people/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import { z } from "zod";
import { updatePerson, deletePerson, getPeople } from "@/lib/people";
import { uploadPersonAvatar, deletePersonAvatar, avatarUrl } from "@/lib/people/avatars";
import { requireAuth } from "@/lib/auth-helpers";
import { isDemoRestrictedSession, requireAuth } from "@/lib/auth-helpers";
import { DEMO_RESTRICTED_ACTION_ERROR } from "@/lib/demo-access";

const patchSchema = z.object({
name: z.string().min(1).max(255).optional(),
Expand All @@ -17,6 +18,9 @@
export async function PATCH(req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const userId = await requireAuth();
if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
if (await isDemoRestrictedSession()) {
return NextResponse.json({ error: DEMO_RESTRICTED_ACTION_ERROR }, { status: 403 });
}
const { id } = await ctx.params;
const form = await req.formData();
const parsed = patchSchema.safeParse({
Expand Down Expand Up @@ -68,6 +72,9 @@
export async function DELETE(_req: NextRequest, ctx: { params: Promise<{ id: string }> }) {
const userId = await requireAuth();
if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
if (await isDemoRestrictedSession()) {
return NextResponse.json({ error: DEMO_RESTRICTED_ACTION_ERROR }, { status: 403 });
}
const { id } = await ctx.params;
try {
// Capture the avatar path before deletion to clean up.
Expand All @@ -77,7 +84,7 @@
if (existing?.avatarPath) await deletePersonAvatar(existing.avatarPath);
return NextResponse.json({ ok: true });
} catch (e) {
const code = (e as any).code;

Check warning on line 87 in frontend/app/api/people/[id]/route.ts

View workflow job for this annotation

GitHub Actions / build-and-test

Unexpected any. Specify a different type
if (code === "SOLE_OWNER") {
return NextResponse.json(
{ error: (e as Error).message, blockers: (e as any).blockers },
Expand Down
6 changes: 5 additions & 1 deletion frontend/app/api/people/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { NextRequest, NextResponse } from "next/server";
import { z } from "zod";
import { getPeople, createPerson, updatePerson } from "@/lib/people";
import { uploadPersonAvatar, avatarUrl } from "@/lib/people/avatars";
import { requireAuth } from "@/lib/auth-helpers";
import { isDemoRestrictedSession, requireAuth } from "@/lib/auth-helpers";
import { DEMO_RESTRICTED_ACTION_ERROR } from "@/lib/demo-access";

export async function GET() {
const userId = await requireAuth();
Expand All @@ -24,6 +25,9 @@ const createSchema = z.object({
export async function POST(req: NextRequest) {
const userId = await requireAuth();
if (!userId) return NextResponse.json({ error: "unauthorized" }, { status: 401 });
if (await isDemoRestrictedSession()) {
return NextResponse.json({ error: DEMO_RESTRICTED_ACTION_ERROR }, { status: 403 });
}
const form = await req.formData();
const parsed = createSchema.safeParse({
name: form.get("name"),
Expand Down
44 changes: 25 additions & 19 deletions frontend/components/household/people-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Button } from "@/components/ui/button";
import { PersonForm, type PersonFormValues } from "./person-form";
import { PersonAvatar } from "./person-avatar";
import { clearOwnerBadgesCache } from "./owner-badges";
import { DemoReadOnlyNotice } from "@/components/settings/demo-readonly-notice";

type Person = {
id: string;
Expand All @@ -23,7 +24,8 @@ function buildFormData(values: PersonFormValues): FormData {
return fd;
}

export function PeopleList(props: { initialPeople: Person[] }) {
export function PeopleList(props: { initialPeople: Person[]; readOnly?: boolean }) {
const readOnly = props.readOnly ?? false;
const [people, setPeople] = useState(props.initialPeople);
const [adding, setAdding] = useState(false);
const [editingId, setEditingId] = useState<string | null>(null);
Expand Down Expand Up @@ -63,6 +65,7 @@ export function PeopleList(props: { initialPeople: Person[] }) {

return (
<div className="space-y-4">
{readOnly && <DemoReadOnlyNotice />}
<ul className="divide-y rounded-md border">
{people.map((p) => (
<li key={p.id} className="flex items-center gap-3 p-3">
Expand All @@ -71,10 +74,12 @@ export function PeopleList(props: { initialPeople: Person[] }) {
{p.kind === "self" && (
<span className="text-xs text-muted-foreground">you</span>
)}
<Button variant="ghost" size="sm" onClick={() => setEditingId(p.id)}>
Edit
</Button>
{p.kind !== "self" && (
{!readOnly && (
<Button variant="ghost" size="sm" onClick={() => setEditingId(p.id)}>
Edit
</Button>
)}
{!readOnly && p.kind !== "self" && (
<Button variant="ghost" size="sm" onClick={() => remove(p.id)}>
Delete
</Button>
Expand All @@ -101,20 +106,21 @@ export function PeopleList(props: { initialPeople: Person[] }) {
</div>
)}

{adding ? (
<div className="rounded-md border p-4">
<h2 className="mb-3 font-medium">Add person</h2>
<PersonForm
submitLabel="Add person"
onSubmit={create}
onCancel={() => setAdding(false)}
/>
</div>
) : (
<Button variant="outline" onClick={() => setAdding(true)}>
Add person
</Button>
)}
{!readOnly &&
(adding ? (
<div className="rounded-md border p-4">
<h2 className="mb-3 font-medium">Add person</h2>
<PersonForm
submitLabel="Add person"
onSubmit={create}
onCancel={() => setAdding(false)}
/>
</div>
) : (
<Button variant="outline" onClick={() => setAdding(true)}>
Add person
</Button>
))}
</div>
);
}
41 changes: 22 additions & 19 deletions frontend/components/onboarding/category-row.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ interface CategoryRowProps {
category: CategoryInput;
onEdit: () => void;
onDelete: () => void;
readOnly?: boolean;
}

export function CategoryRow({ category, onEdit, onDelete }: CategoryRowProps) {
export function CategoryRow({ category, onEdit, onDelete, readOnly = false }: CategoryRowProps) {
const isSystem = category.isSystem ?? false;

return (
Expand Down Expand Up @@ -39,30 +40,32 @@ export function CategoryRow({ category, onEdit, onDelete }: CategoryRowProps) {
</div>

{/* Action buttons */}
<div className="flex items-center gap-1 shrink-0">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={onEdit}
title={isSystem ? "Edit description and categorization instructions" : "Edit category"}
>
<RiEditLine className="h-4 w-4" />
</Button>
{!isSystem && (
{!readOnly && (
<div className="flex items-center gap-1 shrink-0">
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={onDelete}
title="Delete category"
className="h-8 w-8"
onClick={onEdit}
title={isSystem ? "Edit description and categorization instructions" : "Edit category"}
>
<RiDeleteBinLine className="h-4 w-4" />
<RiEditLine className="h-4 w-4" />
</Button>
)}
</div>
{!isSystem && (
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:text-destructive"
onClick={onDelete}
title="Delete category"
>
<RiDeleteBinLine className="h-4 w-4" />
</Button>
)}
</div>
)}
</div>
);
}
24 changes: 15 additions & 9 deletions frontend/components/onboarding/profile-photo-upload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ interface ProfilePhotoUploadProps {
onChange: (file: File | null) => void;
defaultImage?: string | null;
name?: string;
disabled?: boolean;
}

export function ProfilePhotoUpload({
value,
onChange,
defaultImage,
name,
disabled = false,
}: ProfilePhotoUploadProps) {
const [preview, setPreview] = useState<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
Expand Down Expand Up @@ -90,13 +92,14 @@ export function ProfilePhotoUpload({
<div className="flex flex-col items-start gap-4">
<div
className={cn(
"relative cursor-pointer transition-all",
"relative transition-all",
disabled ? "cursor-not-allowed opacity-60" : "cursor-pointer",
isDragging && "ring-2 ring-primary ring-offset-2"
)}
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={() => inputRef.current?.click()}
onDrop={disabled ? undefined : handleDrop}
onDragOver={disabled ? undefined : handleDragOver}
onDragLeave={disabled ? undefined : handleDragLeave}
onClick={disabled ? undefined : () => inputRef.current?.click()}
>
<Avatar className="h-24 w-24">
{displayImage ? (
Expand All @@ -105,10 +108,12 @@ export function ProfilePhotoUpload({
<AvatarFallback className="text-2xl">{getInitials(name)}</AvatarFallback>
)}
</Avatar>
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity hover:opacity-100">
<RiCameraLine className="h-6 w-6 text-white" />
</div>
{displayImage && (
{!disabled && (
<div className="absolute inset-0 flex items-center justify-center bg-black/50 opacity-0 transition-opacity hover:opacity-100">
<RiCameraLine className="h-6 w-6 text-white" />
</div>
)}
{displayImage && !disabled && (
<Button
type="button"
variant="destructive"
Expand All @@ -129,6 +134,7 @@ export function ProfilePhotoUpload({
accept="image/*"
className="hidden"
onChange={handleInputChange}
disabled={disabled}
/>
</div>
);
Expand Down
28 changes: 20 additions & 8 deletions frontend/components/settings/bank-connections-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
AlertDialogTrigger,
} from "@/components/ui/alert-dialog";
import { triggerSync, disconnectBank, triggerRecategorize, initiateAuth } from "@/lib/actions/bank-connections";
import { DemoReadOnlyNotice } from "./demo-readonly-notice";
type BankConnectionItem = {
id: string;
aspspName: string;
Expand All @@ -39,6 +40,7 @@ type BankConnectionItem = {

interface BankConnectionsManagerProps {
connections: BankConnectionItem[];
isDemoUser?: boolean;
}

type SyncProgress = {
Expand All @@ -50,7 +52,7 @@ type SyncProgress = {
started_at?: string;
};

export function BankConnectionsManager({ connections }: BankConnectionsManagerProps) {
export function BankConnectionsManager({ connections, isDemoUser = false }: BankConnectionsManagerProps) {
const router = useRouter();
const [syncingIds, setSyncingIds] = useState<Set<string>>(new Set());
const [recategorizingIds, setRecategorizingIds] = useState<Set<string>>(new Set());
Expand Down Expand Up @@ -309,20 +311,28 @@ export function BankConnectionsManager({ connections }: BankConnectionsManagerPr

return (
<div className="space-y-6">
{isDemoUser && <DemoReadOnlyNotice />}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold">Bank Connections</h2>
<p className="text-sm text-muted-foreground">
Connect your bank accounts via Open Banking to automatically sync transactions.
</p>
</div>
<Link
href="/settings/connect-bank"
className={buttonVariants({ variant: "default", size: "default" })}
>
<RiAddLine className="mr-1.5 h-4 w-4" />
Connect Bank
</Link>
{isDemoUser ? (
<Button variant="default" size="default" disabled>
<RiAddLine className="mr-1.5 h-4 w-4" />
Connect Bank
</Button>
) : (
<Link
href="/settings/connect-bank"
className={buttonVariants({ variant: "default", size: "default" })}
>
<RiAddLine className="mr-1.5 h-4 w-4" />
Connect Bank
</Link>
)}
</div>

{activeConnections.length === 0 ? (
Expand Down Expand Up @@ -405,6 +415,7 @@ export function BankConnectionsManager({ connections }: BankConnectionsManagerPr
)}
</div>
</div>
{!isDemoUser && (
<div className="flex items-center gap-2">
{connection.status === "active" && (
<Button
Expand Down Expand Up @@ -469,6 +480,7 @@ export function BankConnectionsManager({ connections }: BankConnectionsManagerPr
</AlertDialogContent>
</AlertDialog>
</div>
)}
</div>
{syncingIds.has(connection.id) && (
<div className="mt-3 space-y-1.5">
Expand Down
Loading
Loading