diff --git a/app/lib/types/PresetGetResponse.ts b/app/lib/types/PresetGetResponse.ts new file mode 100644 index 0000000..269bebb --- /dev/null +++ b/app/lib/types/PresetGetResponse.ts @@ -0,0 +1,29 @@ +import type { getPresetAuthorData } from "~/lib/utils/presetsDb"; +// below types are ai generated +interface IceServer { + urls: string | string[]; + username?: string; + credential?: string; +} +interface Preset { + id: string; + name: string; + createdBy: string; + iceServers: string | IceServer[]; // Database returns string, we transform to IceServer[] + shareable: boolean; + createdAt: string; +} +interface PresetUser { + id: string; + presetId: string; + userId: string; + isDefault: boolean; + addedAt: string; + preset: Preset; +} + +export interface ApiResponse { + success: boolean; + data: PresetUser[]; + author: Awaited>; +} diff --git a/app/lib/types/PresetShareResponse.ts b/app/lib/types/PresetShareResponse.ts new file mode 100644 index 0000000..24d2c9a --- /dev/null +++ b/app/lib/types/PresetShareResponse.ts @@ -0,0 +1,27 @@ +interface IceServer { + urls: string | string[]; + username?: string; + credential?: string; +} + +export interface Preset { + id: string; + name: string; + createdBy: string; + iceServers: string | IceServer[]; // Database returns string, we transform to IceServer[] + shareable: boolean; + createdAt: string; +} + +export interface PresetAuthor { + id: string; + fullName: string | null; + profileImageUrl: string | null; + username: string | null; +} + +export interface PresetShareResponse { + success: boolean; + data: Preset; + author: PresetAuthor; +} diff --git a/app/lib/utils/presetsDb.ts b/app/lib/utils/presetsDb.ts index 5464da8..3e9002d 100644 --- a/app/lib/utils/presetsDb.ts +++ b/app/lib/utils/presetsDb.ts @@ -1,6 +1,8 @@ +import { clerkClient } from "@clerk/nuxt/server"; import { eq, and } from "drizzle-orm"; import { db } from "~/lib/db/index"; import * as schema from "~/lib/db/schema"; +import type { H3Event } from "h3"; export async function getUserPresets(clerkUserId: string) { return await db.query.presetUsers.findMany({ @@ -124,3 +126,39 @@ export async function updatePresetDefaultStatus( export async function deletePreset(presetId: string) { await db.delete(schema.presets).where(eq(schema.presets.id, presetId)); } + +export async function getOwnedPresets(userId: string) { + return await db.query.presets.findMany({ + where: eq(schema.presets.createdBy, userId), + }); +} + +export async function ownsPreset( + presetId: string, + userId: string, +): Promise { + const preset = await getPresetById(presetId); + if (!preset) return false; + return preset.createdBy === userId; +} + +export async function markAsShareable(presetId: string, shareable: boolean) { + await db + .update(schema.presets) + .set({ shareable }) + .where(eq(schema.presets.id, presetId)); +} + +export async function getPresetAuthorData(event: H3Event, presetId: string) { + const preset = await getPresetById(presetId); + if (!preset) { + throw createError({ statusCode: 404, statusMessage: "Preset not found" }); + } + const user = await clerkClient(event).users.getUser(preset.createdBy); + return { + id: user.id, + fullName: user.fullName, + profileImageUrl: user.imageUrl, + username: user.username, + }; +} diff --git a/app/pages/presets/index.vue b/app/pages/presets/index.vue index 86bc658..8986339 100644 --- a/app/pages/presets/index.vue +++ b/app/pages/presets/index.vue @@ -67,7 +67,6 @@ diff --git a/app/pages/presets/shared/[id].vue b/app/pages/presets/shared/[id].vue new file mode 100644 index 0000000..3c9259b --- /dev/null +++ b/app/pages/presets/shared/[id].vue @@ -0,0 +1,145 @@ + + + diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c95b81..26b40ed 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -272,16 +272,32 @@ packages: react-dom: optional: true + '@clerk/shared@3.42.0': + resolution: {integrity: sha512-sJUur/7jnHHlAsdoDosxpOmfV05VR7K5rvqlFskj3GaAMFEJrvdOztw0hmhBGVSWiCpjTZfdGITegton8mo7mQ==} + engines: {node: '>=18.17.0'} + peerDependencies: + react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 + peerDependenciesMeta: + react: + optional: true + react-dom: + optional: true + '@clerk/themes@2.4.46': resolution: {integrity: sha512-26U+aInnWJwYHrT/LYX7sGRrLJwLk4NvfEoUyVc5EXUl7ue/TZ88r7ZiKDv0KutBTNopaz+p+7KD6F1j72nodA==} engines: {node: '>=18.17.0'} + '@clerk/types@4.101.10': + resolution: {integrity: sha512-qlmgnAm/IeK02RKEKVN8/Glx07xw/Lcv67jBfikM8HXhHc5v7bfYLD8UiWTr6H2RGtvB09cIt9JezRRlsuVBew==} + engines: {node: '>=18.17.0'} + '@clerk/types@4.101.9': resolution: {integrity: sha512-RO00JqqmkIoI1o0XCtvudjaLpqEoe8PRDHlLS1r/aNZazUQCO0TT6nZOx1F3X+QJDjqYVY7YmYl3mtO2QVEk1g==} engines: {node: '>=18.17.0'} - '@clerk/vue@1.17.6': - resolution: {integrity: sha512-N2k90EEwFqo7qXU8vVfVJDRHJ5G33ip+IwoWhbuWIk947PvMLZFirmCfX1c3D/xkv1r0qBs6AGocFClllE+X/w==} + '@clerk/vue@1.17.7': + resolution: {integrity: sha512-NnVUIzV+vJeVRqtxHnsp96LLZJcvsKEA4xnC/iogW4PFM0C8Qb12038E5wV/xV3YhBiwnjTEGPmK9rbfFdXgtw==} engines: {node: '>=18.17.0'} peerDependencies: vue: ^3.2.0 @@ -4590,7 +4606,7 @@ snapshots: '@clerk/backend': 2.29.0(react@19.2.3) '@clerk/shared': 3.41.1(react@19.2.3) '@clerk/types': 4.101.9(react@19.2.3) - '@clerk/vue': 1.17.6(react@19.2.3)(vue@3.5.22(typescript@5.9.3)) + '@clerk/vue': 1.17.7(react@19.2.3)(vue@3.5.22(typescript@5.9.3)) '@nuxt/kit': 4.2.0(magicast@0.5.0) '@nuxt/schema': 4.2.0 h3: 1.15.4 @@ -4611,6 +4627,17 @@ snapshots: optionalDependencies: react: 19.2.3 + '@clerk/shared@3.42.0(react@19.2.3)': + dependencies: + csstype: 3.1.3 + dequal: 2.0.3 + glob-to-regexp: 0.4.1 + js-cookie: 3.0.5 + std-env: 3.10.0 + swr: 2.3.4(react@19.2.3) + optionalDependencies: + react: 19.2.3 + '@clerk/themes@2.4.46(react@19.2.3)': dependencies: '@clerk/shared': 3.41.1(react@19.2.3) @@ -4619,6 +4646,13 @@ snapshots: - react - react-dom + '@clerk/types@4.101.10(react@19.2.3)': + dependencies: + '@clerk/shared': 3.42.0(react@19.2.3) + transitivePeerDependencies: + - react + - react-dom + '@clerk/types@4.101.9(react@19.2.3)': dependencies: '@clerk/shared': 3.41.1(react@19.2.3) @@ -4626,10 +4660,10 @@ snapshots: - react - react-dom - '@clerk/vue@1.17.6(react@19.2.3)(vue@3.5.22(typescript@5.9.3))': + '@clerk/vue@1.17.7(react@19.2.3)(vue@3.5.22(typescript@5.9.3))': dependencies: - '@clerk/shared': 3.41.1(react@19.2.3) - '@clerk/types': 4.101.9(react@19.2.3) + '@clerk/shared': 3.42.0(react@19.2.3) + '@clerk/types': 4.101.10(react@19.2.3) vue: 3.5.22(typescript@5.9.3) transitivePeerDependencies: - react diff --git a/server/api/presets/[id].get.ts b/server/api/presets/[id].get.ts index 3596657..3138ae0 100644 --- a/server/api/presets/[id].get.ts +++ b/server/api/presets/[id].get.ts @@ -1,4 +1,8 @@ -import { getPresetById, userHasPresetAccess } from "~/lib/utils/presetsDb"; +import { + getPresetAuthorData, + getPresetById, + userHasPresetAccess, +} from "~/lib/utils/presetsDb"; export default defineEventHandler(async (event) => { const { isAuthenticated, userId } = event.context.auth(); @@ -12,21 +16,15 @@ export default defineEventHandler(async (event) => { throw createError({ statusCode: 400, statusMessage: "Missing preset ID" }); } - // Fetch the preset const preset = await getPresetById(id); - if (!preset) { throw createError({ statusCode: 404, statusMessage: "Preset not found" }); } - - // Check if user has access - const hasAccess = await userHasPresetAccess(id, userId); - if (!hasAccess) { - throw createError({ statusCode: 403, statusMessage: "Forbidden" }); - } + const author = await getPresetAuthorData(event, id); return { success: true, data: preset, + author, }; }); diff --git a/server/api/presets/[id]/import.post.ts b/server/api/presets/[id]/import.post.ts new file mode 100644 index 0000000..f77214d --- /dev/null +++ b/server/api/presets/[id]/import.post.ts @@ -0,0 +1,69 @@ +import { getPresetById, userHasPresetAccess } from "~/lib/utils/presetsDb"; +import { db } from "~/lib/db/index"; +import * as schema from "~/lib/db/schema"; + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, "id"); + const { isAuthenticated, userId } = event.context.auth(); + + if (!isAuthenticated || !userId) { + setResponseStatus(event, 401); + return { + success: false, + message: "Unauthorized", + }; + } + + if (!id) { + setResponseStatus(event, 400); + return { + success: false, + message: "Missing preset ID", + }; + } + + const preset = await getPresetById(id); + if (!preset) { + setResponseStatus(event, 404); + return { + success: false, + message: "Preset not found", + }; + } + + if (!preset.shareable) { + setResponseStatus(event, 403); + return { + success: false, + message: "This preset is not shareable", + }; + } + + // Check if user already has this preset + const userAlreadyHasPreset = await db.query.presetUsers.findFirst({ + where: (presetUsers, { eq, and }) => + and( + eq(presetUsers.presetId, id), + eq(presetUsers.userId, userId), + ), + }); + + if (userAlreadyHasPreset) { + return { + success: false, + message: "You already have this preset imported", + }; + } + + // Add preset to user + await db.insert(schema.presetUsers).values({ + presetId: id, + userId: userId, + isDefault: false, + }); + + return { + success: true, + message: "Preset imported successfully", + }; +}); diff --git a/server/api/presets/[id]/share.post.ts b/server/api/presets/[id]/share.post.ts index e69de29..b7969e0 100644 --- a/server/api/presets/[id]/share.post.ts +++ b/server/api/presets/[id]/share.post.ts @@ -0,0 +1,36 @@ +import { markAsShareable, ownsPreset } from "~/lib/utils/presetsDb"; + +export default defineEventHandler(async (event) => { + const id = getRouterParam(event, "id"); + const { isAuthenticated, userId } = event.context.auth(); + + if (!isAuthenticated || !userId) { + setResponseStatus(event, 401); + return { + success: false, + message: "Unauthorized", + }; + } + if (!id) { + setResponseStatus(event, 400); + return { + success: false, + message: "Missing preset ID", + }; + } + + if (!(await ownsPreset(id, userId))) { + setResponseStatus(event, 403); + return { + success: false, + message: "Forbidden", + }; + } + + await markAsShareable(id, true); + + return { + success: true, + message: "Preset marked as shareable", + }; +});