Skip to content

Commit f378ee9

Browse files
committed
fix issue with deleting files on personal account
1 parent 7499554 commit f378ee9

File tree

8 files changed

+334
-32
lines changed

8 files changed

+334
-32
lines changed

README.md

-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ Check out our [Next.js deployment documentation](https://nextjs.org/docs/deploym
3737

3838
## TODO
3939

40-
- filters by type
4140
- shared with me
42-
- table view
43-
- view toggle
4441
- folders
4542
- landing page

convex/files.ts

+22-16
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {
88
} from "./_generated/server";
99
import { getUser } from "./users";
1010
import { fileTypes } from "./schema";
11-
import { Id } from "./_generated/dataModel";
11+
import { Doc, Id } from "./_generated/dataModel";
1212

1313
export const generateUploadUrl = mutation(async (ctx) => {
1414
const identity = await ctx.auth.getUserIdentity();
@@ -20,7 +20,10 @@ export const generateUploadUrl = mutation(async (ctx) => {
2020
return await ctx.storage.generateUploadUrl();
2121
});
2222

23-
async function hasAccessToOrg(ctx: QueryCtx | MutationCtx, orgId: string) {
23+
export async function hasAccessToOrg(
24+
ctx: QueryCtx | MutationCtx,
25+
orgId: string
26+
) {
2427
const identity = await ctx.auth.getUserIdentity();
2528

2629
if (!identity) {
@@ -79,6 +82,7 @@ export const getFiles = query({
7982
query: v.optional(v.string()),
8083
favorites: v.optional(v.boolean()),
8184
deletedOnly: v.optional(v.boolean()),
85+
type: v.optional(fileTypes),
8286
},
8387
async handler(ctx, args) {
8488
const hasAccess = await hasAccessToOrg(ctx, args.orgId);
@@ -119,6 +123,10 @@ export const getFiles = query({
119123
files = files.filter((file) => !file.shouldDelete);
120124
}
121125

126+
if (args.type) {
127+
files = files.filter((file) => file.type === args.type);
128+
}
129+
122130
return files;
123131
},
124132
});
@@ -140,6 +148,16 @@ export const deleteAllFiles = internalMutation({
140148
},
141149
});
142150

151+
function assertCanDeleteFile(user: Doc<"users">, file: Doc<"files">) {
152+
const canDelete =
153+
file.userId === user._id ||
154+
user.orgIds.find((org) => org.orgId === file.orgId)?.role === "admin";
155+
156+
if (!canDelete) {
157+
throw new ConvexError("you have no acces to delete this file");
158+
}
159+
}
160+
143161
export const deleteFile = mutation({
144162
args: { fileId: v.id("files") },
145163
async handler(ctx, args) {
@@ -149,13 +167,7 @@ export const deleteFile = mutation({
149167
throw new ConvexError("no access to file");
150168
}
151169

152-
const isAdmin =
153-
access.user.orgIds.find((org) => org.orgId === access.file.orgId)
154-
?.role === "admin";
155-
156-
if (!isAdmin) {
157-
throw new ConvexError("you have no admin access to delete");
158-
}
170+
assertCanDeleteFile(access.user, access.file);
159171

160172
await ctx.db.patch(args.fileId, {
161173
shouldDelete: true,
@@ -172,13 +184,7 @@ export const restoreFile = mutation({
172184
throw new ConvexError("no access to file");
173185
}
174186

175-
const isAdmin =
176-
access.user.orgIds.find((org) => org.orgId === access.file.orgId)
177-
?.role === "admin";
178-
179-
if (!isAdmin) {
180-
throw new ConvexError("you have no admin access to delete");
181-
}
187+
assertCanDeleteFile(access.user, access.file);
182188

183189
await ctx.db.patch(args.fileId, {
184190
shouldDelete: false,

convex/users.ts

+20
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
query,
77
} from "./_generated/server";
88
import { roles } from "./schema";
9+
import { hasAccessToOrg } from "./files";
910

1011
export async function getUser(
1112
ctx: QueryCtx | MutationCtx,
@@ -101,3 +102,22 @@ export const getUserProfile = query({
101102
};
102103
},
103104
});
105+
106+
export const getMe = query({
107+
args: {},
108+
async handler(ctx) {
109+
const identity = await ctx.auth.getUserIdentity();
110+
111+
if (!identity) {
112+
return null;
113+
}
114+
115+
const user = await getUser(ctx, identity.tokenIdentifier);
116+
117+
if (!user) {
118+
return null;
119+
}
120+
121+
return user;
122+
},
123+
});

package-lock.json

+69
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"@radix-ui/react-dialog": "^1.0.5",
1818
"@radix-ui/react-dropdown-menu": "^2.0.6",
1919
"@radix-ui/react-label": "^2.0.2",
20+
"@radix-ui/react-select": "^2.0.0",
2021
"@radix-ui/react-slot": "^1.0.2",
2122
"@radix-ui/react-tabs": "^1.0.4",
2223
"@radix-ui/react-toast": "^1.1.5",

src/app/dashboard/_components/file-actions.tsx

+12-2
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import {
2525
AlertDialogTitle,
2626
} from "@/components/ui/alert-dialog";
2727
import { useState } from "react";
28-
import { useMutation } from "convex/react";
28+
import { useMutation, useQuery } from "convex/react";
2929
import { api } from "../../../../convex/_generated/api";
3030
import { useToast } from "@/components/ui/use-toast";
3131
import { Protect } from "@clerk/nextjs";
@@ -41,6 +41,7 @@ export function FileCardActions({
4141
const restoreFile = useMutation(api.files.restoreFile);
4242
const toggleFavorite = useMutation(api.files.toggleFavorite);
4343
const { toast } = useToast();
44+
const me = useQuery(api.users.getMe);
4445

4546
const [isConfirmOpen, setIsConfirmOpen] = useState(false);
4647

@@ -108,7 +109,16 @@ export function FileCardActions({
108109
)}
109110
</DropdownMenuItem>
110111

111-
<Protect role="org:admin" fallback={<></>}>
112+
<Protect
113+
condition={(check) => {
114+
return (
115+
check({
116+
role: "org:admin",
117+
}) || file.userId === me?._id
118+
);
119+
}}
120+
fallback={<></>}
121+
>
112122
<DropdownMenuSeparator />
113123
<DropdownMenuItem
114124
onClick={() => {

src/app/dashboard/_components/file-browser.tsx

+50-11
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,15 @@ import { useState } from "react";
1111
import { DataTable } from "./file-table";
1212
import { columns } from "./columns";
1313
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectItem,
18+
SelectTrigger,
19+
SelectValue,
20+
} from "@/components/ui/select";
21+
import { Doc } from "../../../../convex/_generated/dataModel";
22+
import { Label } from "@/components/ui/label";
1423

1524
function Placeholder() {
1625
return (
@@ -39,6 +48,7 @@ export function FileBrowser({
3948
const organization = useOrganization();
4049
const user = useUser();
4150
const [query, setQuery] = useState("");
51+
const [type, setType] = useState<Doc<"files">["type"] | "all">("all");
4252

4353
let orgId: string | undefined = undefined;
4454
if (organization.isLoaded && user.isLoaded) {
@@ -52,7 +62,15 @@ export function FileBrowser({
5262

5363
const files = useQuery(
5464
api.files.getFiles,
55-
orgId ? { orgId, query, favorites: favoritesOnly, deletedOnly } : "skip"
65+
orgId
66+
? {
67+
orgId,
68+
type: type === "all" ? undefined : type,
69+
query,
70+
favorites: favoritesOnly,
71+
deletedOnly,
72+
}
73+
: "skip"
5674
);
5775
const isLoading = files === undefined;
5876

@@ -75,15 +93,37 @@ export function FileBrowser({
7593
</div>
7694

7795
<Tabs defaultValue="grid">
78-
<TabsList className="mb-2">
79-
<TabsTrigger value="grid" className="flex gap-2 items-center">
80-
<GridIcon />
81-
Grid
82-
</TabsTrigger>
83-
<TabsTrigger value="table" className="flex gap-2 items-center">
84-
<RowsIcon /> Table
85-
</TabsTrigger>
86-
</TabsList>
96+
<div className="flex justify-between items-center">
97+
<TabsList className="mb-2">
98+
<TabsTrigger value="grid" className="flex gap-2 items-center">
99+
<GridIcon />
100+
Grid
101+
</TabsTrigger>
102+
<TabsTrigger value="table" className="flex gap-2 items-center">
103+
<RowsIcon /> Table
104+
</TabsTrigger>
105+
</TabsList>
106+
107+
<div className="flex gap-2 items-center">
108+
<Label htmlFor="type-select">Type Filter</Label>
109+
<Select
110+
value={type}
111+
onValueChange={(newType) => {
112+
setType(newType as any);
113+
}}
114+
>
115+
<SelectTrigger id="type-select" className="w-[180px]">
116+
<SelectValue />
117+
</SelectTrigger>
118+
<SelectContent>
119+
<SelectItem value="all">All</SelectItem>
120+
<SelectItem value="image">Image</SelectItem>
121+
<SelectItem value="csv">CSV</SelectItem>
122+
<SelectItem value="pdf">PDF</SelectItem>
123+
</SelectContent>
124+
</Select>
125+
</div>
126+
</div>
87127

88128
{isLoading && (
89129
<div className="flex flex-col gap-8 w-full items-center mt-24">
@@ -100,7 +140,6 @@ export function FileBrowser({
100140
</div>
101141
</TabsContent>
102142
<TabsContent value="table">
103-
{" "}
104143
<DataTable columns={columns} data={modifiedFiles} />
105144
</TabsContent>
106145
</Tabs>

0 commit comments

Comments
 (0)