fix: resolve lint errors from ACL/admin implementation

- Remove unused requireOwnerOrAdmin import from videos.ts
- Remove unused requireAuth import from users.ts
- Remove unused GraphQLError import from articles.ts
- Replace URLSearchParams with SvelteURLSearchParams in admin users page
- Apply prettier formatting to all changed files

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 12:35:11 +01:00
parent c1770ab9c9
commit ad7ceee5f8
18 changed files with 112 additions and 130 deletions

View File

@@ -1,4 +1,3 @@
import { GraphQLError } from "graphql";
import { builder } from "../builder";
import { ArticleType } from "../types/index";
import { articles, users } from "../../db/schema/index";
@@ -80,10 +79,7 @@ builder.queryField("adminListArticles", (t) =>
type: [ArticleType],
resolve: async (_root, _args, ctx) => {
requireRole(ctx, "admin");
const articleList = await ctx.db
.select()
.from(articles)
.orderBy(desc(articles.publish_date));
const articleList = await ctx.db.select().from(articles).orderBy(desc(articles.publish_date));
return Promise.all(articleList.map((article: any) => enrichArticle(ctx.db, article)));
},
}),

View File

@@ -87,11 +87,7 @@ builder.mutationField("deleteComment", (t) =>
id: t.arg.int({ required: true }),
},
resolve: async (_root, args, ctx) => {
const comment = await ctx.db
.select()
.from(comments)
.where(eq(comments.id, args.id))
.limit(1);
const comment = await ctx.db.select().from(comments).where(eq(comments.id, args.id)).limit(1);
if (!comment[0]) throw new GraphQLError("Comment not found");
requireOwnerOrAdmin(ctx, comment[0].user_id);
await ctx.db.delete(comments).where(eq(comments.id, args.id));

View File

@@ -3,7 +3,7 @@ import { builder } from "../builder";
import { CurrentUserType, UserType, AdminUserListType } from "../types/index";
import { users } from "../../db/schema/index";
import { eq, ilike, or, count, and } from "drizzle-orm";
import { requireAuth, requireRole } from "../../lib/acl";
import { requireRole } from "../../lib/acl";
builder.queryField("me", (t) =>
t.field({

View File

@@ -15,7 +15,7 @@ import {
files,
} from "../../db/schema/index";
import { eq, and, lte, desc, inArray, count } from "drizzle-orm";
import { requireRole, requireOwnerOrAdmin } from "../../lib/acl";
import { requireRole } from "../../lib/acl";
async function enrichVideo(db: any, video: any) {
// Fetch models
@@ -433,10 +433,7 @@ builder.queryField("adminListVideos", (t) =>
type: [VideoType],
resolve: async (_root, _args, ctx) => {
requireRole(ctx, "admin");
const rows = await ctx.db
.select()
.from(videos)
.orderBy(desc(videos.upload_date));
const rows = await ctx.db.select().from(videos).orderBy(desc(videos.upload_date));
return Promise.all(rows.map((v: any) => enrichVideo(ctx.db, v)));
},
}),

View File

@@ -229,13 +229,11 @@ export const VideoPlayResponseType = builder
}),
});
export const VideoLikeStatusType = builder
.objectRef<VideoLikeStatus>("VideoLikeStatus")
.implement({
fields: (t) => ({
liked: t.exposeBoolean("liked"),
}),
});
export const VideoLikeStatusType = builder.objectRef<VideoLikeStatus>("VideoLikeStatus").implement({
fields: (t) => ({
liked: t.exposeBoolean("liked"),
}),
});
export const VideoAnalyticsType = builder.objectRef<VideoAnalytics>("VideoAnalytics").implement({
fields: (t) => ({

View File

@@ -26,7 +26,17 @@ function createLogger(bindings: Record<string, unknown> = {}, initialLevel: LogL
message = arg;
} else if (arg !== null && typeof arg === "object") {
// Pino-style: log(obj, msg?) — strip internal pino keys
const { msg: m, level: _l, time: _t, pid: _p, hostname: _h, req: _req, res: _res, reqId, ...rest } = arg as Record<string, unknown>;
const {
msg: m,
level: _l,
time: _t,
pid: _p,
hostname: _h,
req: _req,
res: _res,
reqId,
...rest
} = arg as Record<string, unknown>;
message = msg || (typeof m === "string" ? m : "");
if (reqId) meta.reqId = reqId;
Object.assign(meta, rest);

View File

@@ -90,7 +90,10 @@
{/each}
{#if (recording.device_info?.length ?? 0) > 2}
<div class="text-xs text-muted-foreground/60 px-2">
+{(recording.device_info?.length ?? 0) - 2} more device{(recording.device_info?.length ?? 0) - 2 > 1
+{(recording.device_info?.length ?? 0) - 2} more device{(recording.device_info?.length ??
0) -
2 >
1
? "s"
: ""}
</div>

View File

@@ -16,9 +16,7 @@
<div class="flex min-h-screen bg-background">
<!-- Sidebar -->
<aside
class="w-56 shrink-0 border-r border-border/40 bg-card/60 backdrop-blur-sm flex flex-col"
>
<aside class="w-56 shrink-0 border-r border-border/40 bg-card/60 backdrop-blur-sm flex flex-col">
<div class="px-4 py-5 border-b border-border/40">
<a href="/" class="text-xs text-muted-foreground hover:text-foreground transition-colors">
← Back to site

View File

@@ -21,14 +21,12 @@
let tags = $state<string[]>(data.article.tags ?? []);
let featured = $state(data.article.featured ?? false);
let publishDate = $state(
data.article.publish_date
? new Date(data.article.publish_date).toISOString().slice(0, 16)
: "",
data.article.publish_date ? new Date(data.article.publish_date).toISOString().slice(0, 16) : "",
);
let imageId = $state<string | null>(data.article.image ?? null);
let saving = $state(false);
let preview = $derived(content ? marked.parse(content) as string : "");
let preview = $derived(content ? (marked.parse(content) as string) : "");
async function handleImageUpload(files: File[]) {
const file = files[0];
@@ -98,10 +96,7 @@
<div class="space-y-1.5">
<Label>Content (Markdown)</Label>
<div class="grid grid-cols-2 gap-4 min-h-96">
<Textarea
bind:value={content}
class="h-full min-h-96 font-mono text-sm resize-none"
/>
<Textarea bind:value={content} class="h-full min-h-96 font-mono text-sm resize-none" />
<div
class="rounded-lg border border-border/40 bg-muted/20 p-4 overflow-auto prose prose-sm max-w-none prose-headings:text-foreground prose-p:text-muted-foreground"
>
@@ -117,13 +112,13 @@
<div class="space-y-1.5">
<Label>Cover image</Label>
{#if imageId}
<img src={getAssetUrl(imageId, "thumbnail")} alt="" class="h-24 rounded object-cover mb-2" />
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone
accept="image/*"
maxFileSize={10 * MEGABYTE}
onUpload={handleImageUpload}
/>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
<div class="grid grid-cols-2 gap-4">

View File

@@ -21,7 +21,7 @@
let imageId = $state<string | null>(null);
let saving = $state(false);
let preview = $derived(content ? marked.parse(content) as string : "");
let preview = $derived(content ? (marked.parse(content) as string) : "");
function generateSlug(t: string) {
return t
@@ -87,7 +87,9 @@
<Input
id="title"
bind:value={title}
oninput={() => { if (!slug) slug = generateSlug(title); }}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder="Article title"
/>
</div>
@@ -125,11 +127,7 @@
<div class="space-y-1.5">
<Label>Cover image</Label>
<FileDropZone
accept="image/*"
maxFileSize={10 * MEGABYTE}
onUpload={handleImageUpload}
/>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">Image uploaded ✓</p>{/if}
</div>

View File

@@ -1,6 +1,7 @@
<script lang="ts">
import { goto, invalidateAll } from "$app/navigation";
import { page } from "$app/state";
import { SvelteURLSearchParams } from "svelte/reactivity";
import { toast } from "svelte-sonner";
import { adminUpdateUser, adminDeleteUser } from "$lib/services";
import { getAssetUrl } from "$lib/api";
@@ -25,7 +26,7 @@
function debounceSearch(value: string) {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const params = new URLSearchParams(page.url.searchParams);
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
if (value) params.set("search", value);
else params.delete("search");
params.delete("offset");
@@ -34,7 +35,7 @@
}
function setRole(role: string) {
const params = new URLSearchParams(page.url.searchParams);
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
if (role) params.set("role", role);
else params.delete("role");
params.delete("offset");
@@ -193,7 +194,7 @@
variant="outline"
disabled={data.offset === 0}
onclick={() => {
const params = new URLSearchParams(page.url.searchParams);
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(Math.max(0, data.offset - data.limit)));
goto(`?${params.toString()}`);
}}
@@ -205,7 +206,7 @@
variant="outline"
disabled={data.offset + data.limit >= data.total}
onclick={() => {
const params = new URLSearchParams(page.url.searchParams);
const params = new SvelteURLSearchParams(page.url.searchParams.toString());
params.set("offset", String(data.offset + data.limit));
goto(`?${params.toString()}`);
}}
@@ -230,11 +231,7 @@
</Dialog.Header>
<Dialog.Footer>
<Button variant="outline" onclick={() => (deleteOpen = false)}>Cancel</Button>
<Button
variant="destructive"
disabled={deleting}
onclick={handleDelete}
>
<Button variant="destructive" disabled={deleting} onclick={handleDelete}>
{deleting ? "Deleting…" : "Delete"}
</Button>
</Dialog.Footer>

View File

@@ -87,8 +87,7 @@
>
{/if}
{#if video.featured}
<span
class="px-1.5 py-0.5 rounded text-xs font-medium bg-primary/10 text-primary"
<span class="px-1.5 py-0.5 rounded text-xs font-medium bg-primary/10 text-primary"
>Featured</span
>
{/if}

View File

@@ -19,9 +19,7 @@
let premium = $state(data.video.premium ?? false);
let featured = $state(data.video.featured ?? false);
let uploadDate = $state(
data.video.upload_date
? new Date(data.video.upload_date).toISOString().slice(0, 16)
: "",
data.video.upload_date ? new Date(data.video.upload_date).toISOString().slice(0, 16) : "",
);
let imageId = $state<string | null>(data.video.image ?? null);
let movieId = $state<string | null>(data.video.movie ?? null);
@@ -118,13 +116,13 @@
<div class="space-y-1.5">
<Label>Cover image</Label>
{#if imageId}
<img src={getAssetUrl(imageId, "thumbnail")} alt="" class="h-24 rounded object-cover mb-2" />
<img
src={getAssetUrl(imageId, "thumbnail")}
alt=""
class="h-24 rounded object-cover mb-2"
/>
{/if}
<FileDropZone
accept="image/*"
maxFileSize={10 * MEGABYTE}
onUpload={handleImageUpload}
/>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
</div>
<div class="space-y-1.5">
@@ -132,11 +130,7 @@
{#if movieId}
<p class="text-xs text-muted-foreground mb-1">Current file: {movieId}</p>
{/if}
<FileDropZone
accept="video/*"
maxFileSize={2000 * MEGABYTE}
onUpload={handleVideoUpload}
/>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
</div>
<div class="space-y-1.5">

View File

@@ -110,7 +110,9 @@
<Input
id="title"
bind:value={title}
oninput={() => { if (!slug) slug = generateSlug(title); }}
oninput={() => {
if (!slug) slug = generateSlug(title);
}}
placeholder="Video title"
/>
</div>
@@ -122,26 +124,23 @@
<div class="space-y-1.5">
<Label for="description">Description</Label>
<Textarea id="description" bind:value={description} placeholder="Optional description" rows={3} />
<Textarea
id="description"
bind:value={description}
placeholder="Optional description"
rows={3}
/>
</div>
<div class="space-y-1.5">
<Label>Cover image</Label>
<FileDropZone
accept="image/*"
maxFileSize={10 * MEGABYTE}
onUpload={handleImageUpload}
/>
<FileDropZone accept="image/*" maxFileSize={10 * MEGABYTE} onUpload={handleImageUpload} />
{#if imageId}<p class="text-xs text-green-600 mt-1">Image uploaded ✓</p>{/if}
</div>
<div class="space-y-1.5">
<Label>Video file</Label>
<FileDropZone
accept="video/*"
maxFileSize={2000 * MEGABYTE}
onUpload={handleVideoUpload}
/>
<FileDropZone accept="video/*" maxFileSize={2000 * MEGABYTE} onUpload={handleVideoUpload} />
{#if movieId}<p class="text-xs text-green-600 mt-1">Video uploaded ✓</p>{/if}
</div>

View File

@@ -141,37 +141,37 @@
<!-- Author Bio -->
{#if data.article.author}
<Card class="p-0 bg-gradient-to-r from-card/50 to-card">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<img
src={getAssetUrl(data.article.author.avatar, "mini")}
alt={data.article.author.first_name}
class="w-16 h-16 rounded-full object-cover ring-2 ring-primary/20"
/>
<div class="flex-1">
<h3 class="font-semibold text-lg mb-2">
About {data.article.author.first_name}
</h3>
{#if data.article.author.description}
<p class="text-muted-foreground mb-4">
{data.article.author.description}
</p>
{/if}
{#if data.article.author.website}
<div class="flex gap-4 text-sm">
<a
href={"https://" + data.article.author.website}
class="text-primary hover:underline"
>
{data.article.author.website}
</a>
</div>
{/if}
<Card class="p-0 bg-gradient-to-r from-card/50 to-card">
<CardContent class="p-6">
<div class="flex items-start gap-4">
<img
src={getAssetUrl(data.article.author.avatar, "mini")}
alt={data.article.author.first_name}
class="w-16 h-16 rounded-full object-cover ring-2 ring-primary/20"
/>
<div class="flex-1">
<h3 class="font-semibold text-lg mb-2">
About {data.article.author.first_name}
</h3>
{#if data.article.author.description}
<p class="text-muted-foreground mb-4">
{data.article.author.description}
</p>
{/if}
{#if data.article.author.website}
<div class="flex gap-4 text-sm">
<a
href={"https://" + data.article.author.website}
class="text-primary hover:underline"
>
{data.article.author.website}
</a>
</div>
{/if}
</div>
</div>
</div>
</CardContent>
</Card>
</CardContent>
</Card>
{/if}
</article>

View File

@@ -7,7 +7,9 @@ export const GET = async () => {
excludeRoutePatterns: ["^/signup/verify", "^/password/reset", "^/me", "^/play", "^/tags/.+"],
paramValues: {
"/magazine/[slug]": (await getArticles(fetch)).map((a) => a.slug),
"/models/[slug]": (await getModels(fetch)).map((a) => a.slug).filter((s): s is string => s !== null),
"/models/[slug]": (await getModels(fetch))
.map((a) => a.slug)
.filter((s): s is string => s !== null),
"/videos/[slug]": (await getVideos(fetch)).map((a) => a.slug),
},
defaultChangefreq: "always",

View File

@@ -27,7 +27,7 @@
<Meta
title={displayName}
description={data.user.description || `${displayName}'s profile`}
image={data.user.avatar ? getAssetUrl(data.user.avatar, "thumbnail") ?? undefined : undefined}
image={data.user.avatar ? (getAssetUrl(data.user.avatar, "thumbnail") ?? undefined) : undefined}
/>
<div class="relative min-h-screen bg-gradient-to-br from-background via-primary/5 to-accent/5">
@@ -91,8 +91,6 @@
>
</div>
{#if data.user.description}
<p class="text-muted-foreground mb-4">
{data.user.description}
@@ -183,7 +181,7 @@
{$_("gamification.achievements")} ({data.gamification.achievements.length})
</h3>
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
{#each (data.gamification?.achievements ?? []) as achievement (achievement.id)}
{#each data.gamification?.achievements ?? [] as achievement (achievement.id)}
<div
class="flex flex-col items-center gap-2 p-3 rounded-lg bg-accent/10 border border-border/30 hover:border-primary/50 transition-colors"
title={achievement.description}
@@ -194,7 +192,9 @@
</span>
{#if achievement.date_unlocked}
<span class="text-xs text-muted-foreground">
{new Date(achievement.date_unlocked).toLocaleDateString($locale ?? undefined)}
{new Date(achievement.date_unlocked).toLocaleDateString(
$locale ?? undefined,
)}
</span>
{/if}
</div>