fix: recording save functionality and authentication

- Fix artist_name null handling in header component with email fallback
- Fix authentication in recording endpoints to use req.accountability
- Change duration field type from integer to double precision for millisecond precision
- Add createRecording service function with proper authentication
- Update play page to use fetch API for recording saves

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Valknar XXX
2025-10-28 05:16:36 +01:00
parent 5bd2d9c215
commit a252da6d9d
5 changed files with 65 additions and 31 deletions

View File

@@ -2259,7 +2259,7 @@ fields:
foreign_key_column: null foreign_key_column: null
- collection: sexy_recordings - collection: sexy_recordings
field: duration field: duration
type: integer type: float
meta: meta:
collection: sexy_recordings collection: sexy_recordings
conditions: null conditions: null
@@ -2282,11 +2282,11 @@ fields:
schema: schema:
name: duration name: duration
table: sexy_recordings table: sexy_recordings
data_type: integer data_type: double precision
default_value: null default_value: null
max_length: null max_length: null
numeric_precision: 32 numeric_precision: 53
numeric_scale: 0 numeric_scale: null
is_nullable: false is_nullable: false
is_unique: false is_unique: false
is_indexed: false is_indexed: false

View File

@@ -60,7 +60,7 @@ export default {
// GET /sexy/recordings - List user's recordings // GET /sexy/recordings - List user's recordings
router.get("/recordings", async (req, res) => { router.get("/recordings", async (req, res) => {
const { accountability } = context; const accountability = req.accountability;
if (!accountability?.user) { if (!accountability?.user) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
@@ -93,7 +93,7 @@ export default {
// GET /sexy/recordings/:id - Get single recording // GET /sexy/recordings/:id - Get single recording
router.get("/recordings/:id", async (req, res) => { router.get("/recordings/:id", async (req, res) => {
const { accountability } = context; const accountability = req.accountability;
if (!accountability?.user) { if (!accountability?.user) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
@@ -122,7 +122,7 @@ export default {
// POST /sexy/recordings - Create new recording // POST /sexy/recordings - Create new recording
router.post("/recordings", async (req, res) => { router.post("/recordings", async (req, res) => {
const { accountability } = context; const accountability = req.accountability;
if (!accountability?.user) { if (!accountability?.user) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
@@ -168,13 +168,17 @@ export default {
res.status(201).json(recording); res.status(201).json(recording);
} catch (error: any) { } catch (error: any) {
res.status(500).json({ error: error.message || "Failed to create recording" }); console.error("Failed to create recording:", error);
res.status(500).json({
error: error.message || "Failed to create recording",
details: error.toString()
});
} }
}); });
// PATCH /sexy/recordings/:id - Update recording // PATCH /sexy/recordings/:id - Update recording
router.patch("/recordings/:id", async (req, res) => { router.patch("/recordings/:id", async (req, res) => {
const { accountability } = context; const accountability = req.accountability;
if (!accountability?.user) { if (!accountability?.user) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }
@@ -217,7 +221,7 @@ export default {
// DELETE /sexy/recordings/:id - Delete (archive) recording // DELETE /sexy/recordings/:id - Delete (archive) recording
router.delete("/recordings/:id", async (req, res) => { router.delete("/recordings/:id", async (req, res) => {
const { accountability } = context; const accountability = req.accountability;
if (!accountability?.user) { if (!accountability?.user) {
return res.status(401).json({ error: "Unauthorized" }); return res.status(401).json({ error: "Unauthorized" });
} }

View File

@@ -127,7 +127,7 @@ function isActiveLink(link: any) {
<LogoutButton <LogoutButton
user={{ user={{
name: authStatus.user!.artist_name, name: authStatus.user!.artist_name || authStatus.user!.email.split('@')[0] || 'User',
avatar: getAssetUrl(authStatus.user!.avatar?.id, 'mini')!, avatar: getAssetUrl(authStatus.user!.avatar?.id, 'mini')!,
email: authStatus.user!.email email: authStatus.user!.email
}} }}

View File

@@ -566,6 +566,38 @@ export async function getRecordings(fetch?: typeof globalThis.fetch) {
); );
} }
export async function createRecording(
recording: {
title: string;
description?: string;
duration: number;
events: unknown[];
device_info: unknown[];
tags?: string[];
status?: string;
},
fetch?: typeof globalThis.fetch,
) {
return loggedApiCall(
"createRecording",
async () => {
const directus = getDirectusInstance(fetch);
const response = await directus.request<Recording>(
customEndpoint({
method: "POST",
path: "/sexy/recordings",
body: JSON.stringify(recording),
headers: {
"Content-Type": "application/json",
},
}),
);
return response;
},
{ title: recording.title, eventCount: recording.events.length },
);
}
export async function deleteRecording(id: string) { export async function deleteRecording(id: string) {
return loggedApiCall( return loggedApiCall(
"deleteRecording", "deleteRecording",

View File

@@ -22,8 +22,6 @@ import DeviceCard from "$lib/components/device-card/device-card.svelte";
import RecordingSaveDialog from "./components/recording-save-dialog.svelte"; import RecordingSaveDialog from "./components/recording-save-dialog.svelte";
import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types"; import type { BluetoothDevice, RecordedEvent, DeviceInfo } from "$lib/types";
import { toast } from "svelte-sonner"; import { toast } from "svelte-sonner";
import { customEndpoint } from "@directus/sdk";
import { getDirectusInstance } from "$lib/directus";
const client = new ButtplugClient("Sexy.Art"); const client = new ButtplugClient("Sexy.Art");
let connected = $state(client.connected); let connected = $state(client.connected);
@@ -191,25 +189,25 @@ async function handleSaveRecording(data: {
})); }));
try { try {
const directus = getDirectusInstance(); const response = await fetch("/api/sexy/recordings", {
await directus.request( method: "POST",
customEndpoint({ headers: {
method: "POST", "Content-Type": "application/json",
path: "/sexy/recordings", },
body: JSON.stringify({ body: JSON.stringify({
title: data.title, title: data.title,
description: data.description, description: data.description,
duration: recordingDuration, duration: recordingDuration,
events: recordedEvents, events: recordedEvents,
device_info: deviceInfo, device_info: deviceInfo,
tags: data.tags, tags: data.tags,
status: "draft", status: "draft",
}),
headers: {
"Content-Type": "application/json",
},
}), }),
); });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
toast.success("Recording saved successfully!"); toast.success("Recording saved successfully!");
showSaveDialog = false; showSaveDialog = false;