From 818566b1c5b1097c63da9e05b1ac59cb4287e51f Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sat, 21 Jun 2025 01:38:46 +0200 Subject: [PATCH] feat: initial channel settings implementation --- apps/web/package.json | 1 + .../app/(protected)/api/stream/info/route.ts | 71 ++- .../channel/[channelName]/page.client.tsx | 515 ++++++++++++++++++ .../settings/channel/[channelName]/page.tsx | 72 +++ .../app/UniversalForm/UniversalForm.tsx | 5 +- .../app/UserCombobox/UserCombobox.tsx | 107 ++++ apps/web/src/components/ui/badge.tsx | 36 ++ apps/web/src/components/ui/tabs.tsx | 66 +++ apps/web/src/lib/auth/resolve.ts | 43 ++ apps/web/src/lib/form/actions.ts | 197 ++++++- apps/web/src/lib/form/zod.ts | 6 + packages/auth/package.json | 3 +- packages/auth/src/index.ts | 2 + yarn.lock | 113 ++++ 14 files changed, 1207 insertions(+), 30 deletions(-) create mode 100644 apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx create mode 100644 apps/web/src/app/(protected)/settings/channel/[channelName]/page.tsx create mode 100644 apps/web/src/components/app/UserCombobox/UserCombobox.tsx create mode 100644 apps/web/src/components/ui/badge.tsx create mode 100644 apps/web/src/components/ui/tabs.tsx diff --git a/apps/web/package.json b/apps/web/package.json index e6e8342..0c2f7fe 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -30,6 +30,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.1", "@radix-ui/react-switch": "^1.1.3", + "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-tooltip": "^1.1.6", "@slack/web-api": "^7.9.1", "@uidotdev/usehooks": "^2.4.1", diff --git a/apps/web/src/app/(protected)/api/stream/info/route.ts b/apps/web/src/app/(protected)/api/stream/info/route.ts index ef79fa1..03ba31f 100644 --- a/apps/web/src/app/(protected)/api/stream/info/route.ts +++ b/apps/web/src/app/(protected)/api/stream/info/route.ts @@ -1,34 +1,55 @@ +// FIXME: THIS EFFING SUCKS OH MY GOD + import { validateRequest } from '@/lib/auth/validate'; -import { prisma } from '@hctv/db'; +import { Prisma, prisma } from '@hctv/db'; import type { NextRequest } from 'next/server'; export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams; const shouldGetOwned = searchParams.get('owned') === 'true'; + const allPersonalChannels = searchParams.get('personal') === 'true'; + const isLive = searchParams.get('live') === 'true'; const { user } = await validateRequest(); - /* const db = await prisma.streamInfo.findMany({ - include: { ownedBy: true }, - }); */ - if (shouldGetOwned) { - if (!user) { - return new Response('No user found in cookies', { status: 401 }); - } - - const db = await prisma.streamInfo.findMany({ - where: { - channel: { - ownerId: user.id, - }, - }, - }); - return Response.json(db); - } else { - const db = await prisma.streamInfo.findMany({ - include: { - channel: true, - } - }); - return Response.json(db); + if ((shouldGetOwned || allPersonalChannels) && !user) { + return new Response('No user found in cookies', { status: 401 }); } -} + + const where: Prisma.StreamInfoWhereInput = {}; + const channelConditions: Prisma.ChannelWhereInput[] = []; + + if (shouldGetOwned && user) { + channelConditions.push({ ownerId: user.id }); + } + + if (allPersonalChannels) { + channelConditions.push({ + personalFor: { + isNot: null + } + }); + } + + if (isLive) { + where.isLive = true; + } + + if (channelConditions.length > 0) { + where.channel = channelConditions.length === 1 + ? channelConditions[0] + : { OR: channelConditions }; + } + + const db = await prisma.streamInfo.findMany({ + where, + include: { + channel: { + include: { + personalFor: true, + } + }, + }, + }); + + return Response.json(db); +} \ No newline at end of file diff --git a/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx new file mode 100644 index 0000000..18b7405 --- /dev/null +++ b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.client.tsx @@ -0,0 +1,515 @@ +'use client'; + +import { useState } from 'react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Separator } from '@/components/ui/separator'; +import { + Settings, + Users, + Key, + Bell, + Trash2, + Shield, + UserPlus, + UserMinus, + Copy, + Check, +} from 'lucide-react'; +import { UniversalForm } from '@/components/app/UniversalForm/UniversalForm'; +import { + updateChannelSettings, + addChannelManager, + removeChannelManager, + deleteChannel, + toggleGlobalChannelNotifs, +} from '@/lib/form/actions'; +import { Switch } from '@/components/ui/switch'; +import { toast } from 'sonner'; +import type { Channel, User, StreamInfo, StreamKey, Follow } from '@hctv/db'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '@/components/ui/dialog'; +import { UserCombobox } from '@/components/app/UserCombobox/UserCombobox'; + +interface ChannelSettingsClientProps { + channel: Channel & { + owner: User; + ownerPersonalChannel: Channel | null; + managers: User[]; + managerPersonalChannels: (Channel | null)[]; + streamInfo: StreamInfo[]; + streamKey: StreamKey | null; + followers: (Follow & { user: { id: string; slack_id: string } })[]; + followerPersonalChannels: (Channel | null)[]; + }; + isOwner: boolean; + currentUser: User; + isPersonal: boolean; +} + +export default function ChannelSettingsClient({ + channel, + isOwner, + currentUser, + isPersonal, +}: ChannelSettingsClientProps) { + const [streamKey, setStreamKey] = useState(channel.streamKey?.key || ''); + const [keyVisible, setKeyVisible] = useState(false); + const [copied, setCopied] = useState(false); + + const copyStreamKey = async () => { + if (streamKey) { + await navigator.clipboard.writeText(streamKey); + setCopied(true); + toast.success('Stream key copied to clipboard'); + setTimeout(() => setCopied(false), 2000); + } + }; + + const regenerateStreamKey = async () => { + try { + const response = await fetch('/api/rtmp/streamKey', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ channel: channel.name }), + }); + + if (response.ok) { + const data = await response.json(); + setStreamKey(data.key); + toast.success('Stream key regenerated successfully'); + } else { + toast.error('Failed to regenerate stream key'); + } + } catch (error) { + toast.error('Failed to regenerate stream key'); + } + }; + console.log(isPersonal) + + return ( +
+
+
+ + + {channel.name[0]?.toUpperCase()} + +
+

{channel.name}

+

Channel Settings

+
+ + {channel.followers.length} follower{channel.followers.length !== 1 ? 's' : ''} + + {isOwner && Owner} +
+
+
+
+ + + + + + General + + + + Streaming + + {!isPersonal && ( + + + Managers + + )} + + + Notifications + + + + + + + General Settings + + Manage your channel's basic information and settings. + + + + { + if (result?.success) { + toast.success('Channel settings updated successfully'); + } + }} + /> + + {false && isOwner && ( + <> + +
+

Danger Zone

+ + + Delete Channel + + Permanently delete this channel. This action cannot be undone. + + + + + + +
+ + )} +
+
+
+ + + + + Streaming Settings + Manage your stream key and streaming configuration. + + +
+
+

Stream Key

+

+ Use this key to start streaming to your channel. Keep it secure! +

+
+
+ +
+ + +
+
+ + +
+ + + +
+

Stream Information

+ {channel.streamInfo.length > 0 ? ( +
+ {channel.streamInfo.map((stream) => ( + { + if (result?.success) { + toast.success('Stream information updated'); + } + }} + /> + ))} +
+ ) : ( +

No stream information available.

+ )} +
+
+
+
+ + {!isPersonal && ( + + + + Channel Managers + + Manage who can help moderate and stream to this channel. + + + +
+
+

Current Managers

+ {isOwner && ( + m.id), + channel.owner.id, + ]} + /> + )} +
+ +
+ {/* Owner */} +
+
+ + + + {channel.owner.slack_id[0]?.toUpperCase()} + + +
+

{channel.ownerPersonalChannel?.name}

+

Channel Owner

+
+
+ + + Owner + +
+ + {/* Managers */} + {channel.managers.map((manager) => { + const personalChannel = channel.managerPersonalChannels.find( + (c) => c?.ownerId === manager.id + ); + return ( +
+
+ + + {personalChannel?.name} + +
+

{personalChannel?.name}

+

Manager

+
+
+ {isOwner && ( + + )} +
+ ); + })} + + {channel.managers.length === 0 && ( +

+ No managers added yet. +

+ )} +
+
+
+
+
+ )} + + + + + Notification Settings + + Configure when and how followers are notified about your streams. + + + +
+
+
+

Stream Notifications

+

+ Send notifications to followers when you go live +

+
+ { + toast.promise(toggleGlobalChannelNotifs(channel.id), { + loading: 'Updating notifications...', + success(data) { + return `${data.toggle ? 'Enabled' : 'Disabled'} global notifications for this channel.` + }, + }) + }} + /> +
+ + + +
+

Followers ({channel.followers.length})

+
+ {channel.followers.map((follower) => { + const personalChannel = channel.followerPersonalChannels.find( + (c) => c?.ownerId === follower.user.id + ); + return ( +
+
+ + {personalChannel?.name} + + + {personalChannel?.name} +
+ + {follower.notifyStream ? 'Notifications On' : 'Notifications Off'} + +
+ ); + })} + {channel.followers.length === 0 && ( +

No followers yet.

+ )} +
+
+
+
+
+
+
+
+ ); +} + +function AddManagerDialog({ + channelId, + existingManagers, +}: { + channelId: string; + existingManagers: string[]; +}) { + const [open, setOpen] = useState(false); + const [channel, setChannel] = useState(''); + + return ( + + + + + + + Add channel manager + + Add a channel manager to help manage your channel during big events or projects. + + + { + setChannel(value); + }} + filter={existingManagers} + value={channel} + /> + + + + + + ); +} diff --git a/apps/web/src/app/(protected)/settings/channel/[channelName]/page.tsx b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.tsx new file mode 100644 index 0000000..e3e131c --- /dev/null +++ b/apps/web/src/app/(protected)/settings/channel/[channelName]/page.tsx @@ -0,0 +1,72 @@ +import { validateRequest } from '@/lib/auth/validate'; +import { prisma } from '@hctv/db'; +import { redirect } from 'next/navigation'; +import ChannelSettingsClient from './page.client'; +import { resolvePersonalChannel } from '@/lib/auth/resolve'; + +export default async function ChannelSettingsPage({ + params, +}: { + params: Promise<{ channelName: string }>; +}) { + const { channelName } = await params; + const { user } = await validateRequest(); + + if (!user) { + redirect('/auth/slack'); + } + + const channel = await prisma.channel.findUnique({ + where: { name: channelName }, + include: { + owner: true, + managers: true, + streamInfo: true, + streamKey: true, + followers: { + include: { + user: { + select: { + id: true, + slack_id: true, + }, + }, + }, + }, + personalFor: true, + }, + }); + + if (!channel) { + redirect('/'); + } + + const isOwner = channel.ownerId === user.id; + const isManager = channel.managers.some((manager) => manager.id === user.id); + + if (!isOwner && !isManager) { + redirect('/'); + } + + const ownerPersonalChannel = await resolvePersonalChannel(channel.ownerId); + const managerPersonalChannels = await Promise.all( + channel.managers.map((manager) => resolvePersonalChannel(manager.id)) + ); + const followerPersonalChannels = await Promise.all( + channel.followers.map((follower) => resolvePersonalChannel(follower.user.id)) + ); + + return ( + + ); +} diff --git a/apps/web/src/components/app/UniversalForm/UniversalForm.tsx b/apps/web/src/components/app/UniversalForm/UniversalForm.tsx index e14fd44..48e84ff 100644 --- a/apps/web/src/components/app/UniversalForm/UniversalForm.tsx +++ b/apps/web/src/components/app/UniversalForm/UniversalForm.tsx @@ -19,12 +19,13 @@ import React from 'react'; import { toast } from 'sonner'; import { Textarea } from '@/components/ui/textarea'; import { cn } from '@/lib/utils'; -import { createChannelSchema, onboardSchema, streamInfoEditSchema } from '@/lib/form/zod'; +import { createChannelSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema } from '@/lib/form/zod'; export const schemaDb = [ { name: 'streamInfoEdit', zod: streamInfoEditSchema }, { name: 'onboard', zod: onboardSchema }, - { name: 'createChannel', zod: createChannelSchema } + { name: 'createChannel', zod: createChannelSchema }, + { name: 'updateChannelSettings', zod: updateChannelSettingsSchema }, ] as const; export function UniversalForm({ diff --git a/apps/web/src/components/app/UserCombobox/UserCombobox.tsx b/apps/web/src/components/app/UserCombobox/UserCombobox.tsx new file mode 100644 index 0000000..f64d6ae --- /dev/null +++ b/apps/web/src/components/app/UserCombobox/UserCombobox.tsx @@ -0,0 +1,107 @@ +'use client'; + +import * as React from 'react'; +import { Check, ChevronsUpDown } from 'lucide-react'; + +import { cn } from '@/lib/utils'; +import { Button } from '@/components/ui/button'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import useSWR from 'swr'; +import { fetcher } from '@/lib/services/swr'; +import { Channel, StreamInfo } from '@hctv/db'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; + +export function UserCombobox(props: Props) { + const [open, setOpen] = React.useState(false); + const [internalValue, setInternalValue] = React.useState(''); + + // Use external value if provided, otherwise use internal state + const value = props.value ?? internalValue; + const setValue = props.onValueChange ?? setInternalValue; + const { + data: fetchedUsers, + error, + isLoading, + } = useSWR( + props.users ? null : '/api/stream/info?personal=true', + fetcher + ); + + const users = props.users || fetchedUsers; + + if (!props.users && error) return
Error loading users
; + if (!props.users && isLoading) return
Loading...
; + return ( + + + + + + + + + No user found. + + {users?.filter(user => !props.filter?.some(filterStr => user.userId === filterStr)).map((user) => ( + { + setValue(currentValue === value ? '' : currentValue); + setOpen(false); + }} + > + + + {user.username[0]} + + {user.username} + + + ))} + + + + + + ); +} + +type APIResponse = (StreamInfo & { channel: Channel })[]; +type Props = { + users?: APIResponse; + value?: string; + filter?: string[]; + onValueChange?: (value: string) => void; +} \ No newline at end of file diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/apps/web/src/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/apps/web/src/components/ui/tabs.tsx b/apps/web/src/components/ui/tabs.tsx new file mode 100644 index 0000000..beebf79 --- /dev/null +++ b/apps/web/src/components/ui/tabs.tsx @@ -0,0 +1,66 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +function Tabs({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsList({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsTrigger({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function TabsContent({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/apps/web/src/lib/auth/resolve.ts b/apps/web/src/lib/auth/resolve.ts index 6b99907..31fae88 100644 --- a/apps/web/src/lib/auth/resolve.ts +++ b/apps/web/src/lib/auth/resolve.ts @@ -43,4 +43,47 @@ export async function resolveFollowedChannels(id?: string) { return null; } return db; +} + +export async function resolvePersonalChannel(id?: string) { + const { user } = await validateRequest(); + const db = await prisma.user.findUnique({ + where: { + id: id ?? user?.id, + }, + select: { + personalChannel: true, + }, + }); + if (!db) { + return null; + } + return db.personalChannel; +} + +export async function resolveUserFromPersonalChannelName(channelName: string) { + const db = await prisma.channel.findUnique({ + where: { + name: channelName, + }, + select: { + personalFor: true, + }, + }); + if (!db) { + return null; + } + return db.personalFor; +} + +export async function resolveStreamInfo(channelId: string) { + const db = await prisma.streamInfo.findFirst({ + where: { + channelId, + }, + }); + if (!db) { + return null; + } + return db; } \ No newline at end of file diff --git a/apps/web/src/lib/form/actions.ts b/apps/web/src/lib/form/actions.ts index 916f98b..c3d44ea 100644 --- a/apps/web/src/lib/form/actions.ts +++ b/apps/web/src/lib/form/actions.ts @@ -4,9 +4,9 @@ import { revalidatePath } from 'next/cache'; import { validateRequest } from '@/lib/auth/validate'; import { prisma } from '@hctv/db'; import zodVerify from '../zodVerify'; -import { createChannelSchema, onboardSchema, streamInfoEditSchema } from './zod'; +import { createChannelSchema, onboardSchema, streamInfoEditSchema, updateChannelSettingsSchema } from './zod'; import { initializeStreamInfo } from '../instrumentation/streamInfo'; -import { resolveFollowedChannels } from '../auth/resolve'; +import { resolveFollowedChannels, resolveStreamInfo, resolveUserFromPersonalChannelName } from '../auth/resolve'; import { genIdenticonUpload } from '../utils/genIdenticonUpload'; export async function editStreamInfo(prev: any, formData: FormData) { @@ -156,4 +156,197 @@ export async function createChannel(prev: any, formData: FormData) { await initializeStreamInfo(createdChannel.id); return { success: true }; +} + +export async function updateChannelSettings(prev: any, formData: FormData) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const zod = await zodVerify(updateChannelSettingsSchema, formData); + if (!zod.success) { + return zod; + } + + const channel = await prisma.channel.findUnique({ + where: { id: zod.data.channelId }, + include: { + owner: true, + managers: true, + }, + }); + + if (!channel) { + return { success: false, error: 'Channel not found' }; + } + + const isOwner = channel.ownerId === user.id; + const isManager = channel.managers.some(manager => manager.id === user.id); + + if (!isOwner && !isManager) { + return { success: false, error: 'Unauthorized' }; + } + + // Only owners can change certain settings + const updateData: any = {}; + if (zod.data.name && isOwner) { + updateData.name = zod.data.name; + } + if (zod.data.pfpUrl) { + updateData.pfpUrl = zod.data.pfpUrl; + } + + await prisma.channel.update({ + where: { id: zod.data.channelId }, + data: updateData, + }); + + revalidatePath(`/settings/channel/${channel.name}`); + return { success: true }; +} + +export async function addChannelManager(channelId: string, userChannel: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const channel = await prisma.channel.findUnique({ + where: { id: channelId, personalFor: null }, + include: { owner: true, managers: true }, + }); + + if (!channel) { + return { success: false, error: 'Channel not found OR is personal.' }; + } + + if (channel.ownerId !== user.id) { + return { success: false, error: 'Only channel owners can add managers' }; + } + + if (channel.ownerId === userChannel) { + return { success: false, error: 'Owner can\'t add themselves as managers' }; + } + + const userDb = await resolveUserFromPersonalChannelName(userChannel); + if (!userDb) { + return { success: false, error: 'User not found' }; + } + if (channel.managers.some((m) => m.id === userDb.id)) { + return { success: false, error: 'User is already a manager' }; + } + + await prisma.channel.update({ + where: { id: channelId }, + data: { + managers: { + connect: { id: userDb.id }, + }, + }, + }); + + revalidatePath(`/settings/channel/${channel.name}`); + return { success: true }; +} + +export async function removeChannelManager(channelId: string, userId: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const channel = await prisma.channel.findUnique({ + where: { id: channelId }, + include: { owner: true }, + }); + + if (!channel) { + return { success: false, error: 'Channel not found' }; + } + + if (channel.ownerId !== user.id) { + return { success: false, error: 'Only channel owners can remove managers' }; + } + + await prisma.channel.update({ + where: { id: channelId }, + data: { + managers: { + disconnect: { id: userId }, + }, + }, + }); + + revalidatePath(`/settings/channel/${channel.name}`); + return { success: true }; +} + +export async function toggleGlobalChannelNotifs(channelId: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const channel = await prisma.channel.findUnique({ + where: { id: channelId }, + include: { followers: true }, + }); + + if (!channel) { + return { success: false, error: 'Channel not found' }; + } + + const streamInfo = await resolveStreamInfo(channelId); + if (!streamInfo) { + return { success: false, error: 'Stream info not found' }; + } + + await prisma.streamInfo.update({ + where: { + id: streamInfo.id, + }, + data: { + enableNotifications: !streamInfo.enableNotifications, + } + }) + + revalidatePath(`/settings/channel/${channel.name}`); + + return { success: true, toggle: !streamInfo.enableNotifications }; +} + +export async function deleteChannel(channelId: string) { + return { success: false, error: 'disabled atm. dm @eth0 if you want to request a deletion.' } + /* const { user } = await validateRequest(); + if (!user) { + return { success: false, error: 'Unauthorized' }; + } + + const channel = await prisma.channel.findUnique({ + where: { id: channelId }, + include: { + owner: true, + personalFor: true, + }, + }); + + if (!channel) { + return { success: false, error: 'Channel not found' }; + } + + if (channel.ownerId !== user.id) { + return { success: false, error: 'Only channel owners can delete channels' }; + } + + // Prevent deletion of personal channels + if (channel.personalFor) { + return { success: false, error: 'Cannot delete personal channels' }; + } + + await prisma.channel.delete({ + where: { id: channelId }, + }); + + return { success: true }; */ } \ No newline at end of file diff --git a/apps/web/src/lib/form/zod.ts b/apps/web/src/lib/form/zod.ts index 32cc63f..f79721b 100644 --- a/apps/web/src/lib/form/zod.ts +++ b/apps/web/src/lib/form/zod.ts @@ -18,4 +18,10 @@ export const onboardSchema = z.object({ export const createChannelSchema = z.object({ name: username, +}); + +export const updateChannelSettingsSchema = z.object({ + channelId: z.string().min(1), + name: username.optional(), + pfpUrl: z.string().url().optional(), }); \ No newline at end of file diff --git a/packages/auth/package.json b/packages/auth/package.json index 16be15f..6557fe4 100644 --- a/packages/auth/package.json +++ b/packages/auth/package.json @@ -10,7 +10,8 @@ }, "type": "module", "scripts": { - "build": "tsc --build" + "build": "tsc --build", + "dev": "tsc --watch --preserveWatchOutput" }, "dependencies": { "@hctv/db": "*", diff --git a/packages/auth/src/index.ts b/packages/auth/src/index.ts index eedfa54..8d17059 100644 --- a/packages/auth/src/index.ts +++ b/packages/auth/src/index.ts @@ -21,6 +21,7 @@ export const lucia = new Lucia(adapter, { slack_id: attributes.slack_id, pfpUrl: attributes.pfpUrl, hasOnboarded: attributes.hasOnboarded, + personalChannelId: attributes.personalChannelId, }; }, }); @@ -36,4 +37,5 @@ interface DatabaseUserAttributes { slack_id: string; pfpUrl: string; hasOnboarded: boolean; + personalChannelId: string | null; } diff --git a/yarn.lock b/yarn.lock index 01becac..70bc57f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1038,6 +1038,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.1.tgz#fc169732d755c7fbad33ba8d0cd7fd10c90dc8e3" integrity sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA== +"@radix-ui/primitive@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.2.tgz#83f415c4425f21e3d27914c12b3272a32e3dae65" + integrity sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA== + "@radix-ui/react-arrow@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz#30c0d574d7bb10eed55cd7007b92d38b03c6b2ab" @@ -1079,6 +1084,16 @@ "@radix-ui/react-primitive" "2.0.2" "@radix-ui/react-slot" "1.1.2" +"@radix-ui/react-collection@1.1.7": + version "1.1.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.7.tgz#d05c25ca9ac4695cc19ba91f42f686e3ea2d9aec" + integrity sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-slot" "1.2.3" + "@radix-ui/react-compose-refs@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" @@ -1091,6 +1106,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz#6f766faa975f8738269ebb8a23bad4f5a8d2faec" integrity sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw== +"@radix-ui/react-compose-refs@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz#a2c4c47af6337048ee78ff6dc0d090b390d2bb30" + integrity sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg== + "@radix-ui/react-context@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" @@ -1103,6 +1123,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a" integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q== +"@radix-ui/react-context@1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.2.tgz#61628ef269a433382c364f6f1e3788a6dc213a36" + integrity sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA== + "@radix-ui/react-dialog@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.0.5.tgz#71657b1b116de6c7a0b03242d7d43e01062c7300" @@ -1149,6 +1174,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc" integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg== +"@radix-ui/react-direction@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.1.tgz#39e5a5769e676c753204b792fbe6cf508e550a14" + integrity sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw== + "@radix-ui/react-dismissable-layer@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" @@ -1231,6 +1261,13 @@ dependencies: "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-id@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.1.tgz#1404002e79a03fe062b7e3864aa01e24bd1471f7" + integrity sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-label@^2.1.1": version "2.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-label/-/react-label-2.1.2.tgz#994a5d815c2ff46e151410ae4e301f1b639f9971" @@ -1332,6 +1369,14 @@ "@radix-ui/react-compose-refs" "1.1.1" "@radix-ui/react-use-layout-effect" "1.1.0" +"@radix-ui/react-presence@1.1.4": + version "1.1.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.4.tgz#253ac0ad4946c5b4a9c66878335f5cf07c967ced" + integrity sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-primitive@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" @@ -1347,6 +1392,28 @@ dependencies: "@radix-ui/react-slot" "1.1.2" +"@radix-ui/react-primitive@2.1.3": + version "2.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz#db9b8bcff49e01be510ad79893fb0e4cda50f1bc" + integrity sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ== + dependencies: + "@radix-ui/react-slot" "1.2.3" + +"@radix-ui/react-roving-focus@1.1.10": + version "1.1.10" + resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz#46030496d2a490c4979d29a7e1252465e51e4b0b" + integrity sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-collection" "1.1.7" + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-use-callback-ref" "1.1.1" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-roving-focus@1.1.2": version "1.1.2" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz#815d051a54299114a68db6eb8d34c41a3c0a646f" @@ -1411,6 +1478,13 @@ dependencies: "@radix-ui/react-compose-refs" "1.1.1" +"@radix-ui/react-slot@1.2.3": + version "1.2.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.2.3.tgz#502d6e354fc847d4169c3bc5f189de777f68cfe1" + integrity sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A== + dependencies: + "@radix-ui/react-compose-refs" "1.1.2" + "@radix-ui/react-switch@^1.1.3": version "1.1.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.1.3.tgz#cb6386909d1d3f65a2b81a3b15da8c91d18f49b0" @@ -1424,6 +1498,20 @@ "@radix-ui/react-use-previous" "1.1.0" "@radix-ui/react-use-size" "1.1.0" +"@radix-ui/react-tabs@^1.1.12": + version "1.1.12" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz#99b3522c73db9263f429a6d0f5a9acb88df3b129" + integrity sha512-GTVAlRVrQrSw3cEARM0nAx73ixrWDPNZAruETn3oHCNP6SbZ/hNxdxp+u7VkIEv3/sFoLq1PfcHrl7Pnp0CDpw== + dependencies: + "@radix-ui/primitive" "1.1.2" + "@radix-ui/react-context" "1.1.2" + "@radix-ui/react-direction" "1.1.1" + "@radix-ui/react-id" "1.1.1" + "@radix-ui/react-presence" "1.1.4" + "@radix-ui/react-primitive" "2.1.3" + "@radix-ui/react-roving-focus" "1.1.10" + "@radix-ui/react-use-controllable-state" "1.2.2" + "@radix-ui/react-tooltip@^1.1.6": version "1.1.8" resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz#1aa2a575630fca2b2845b62f85056bb826bec456" @@ -1454,6 +1542,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1" integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw== +"@radix-ui/react-use-callback-ref@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz#62a4dba8b3255fdc5cc7787faeac1c6e4cc58d40" + integrity sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg== + "@radix-ui/react-use-controllable-state@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" @@ -1469,6 +1562,21 @@ dependencies: "@radix-ui/react-use-callback-ref" "1.1.0" +"@radix-ui/react-use-controllable-state@1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz#905793405de57d61a439f4afebbb17d0645f3190" + integrity sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg== + dependencies: + "@radix-ui/react-use-effect-event" "0.0.2" + "@radix-ui/react-use-layout-effect" "1.1.1" + +"@radix-ui/react-use-effect-event@0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz#090cf30d00a4c7632a15548512e9152217593907" + integrity sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA== + dependencies: + "@radix-ui/react-use-layout-effect" "1.1.1" + "@radix-ui/react-use-escape-keydown@1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" @@ -1496,6 +1604,11 @@ resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27" integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w== +"@radix-ui/react-use-layout-effect@1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz#0c4230a9eed49d4589c967e2d9c0d9d60a23971e" + integrity sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ== + "@radix-ui/react-use-previous@1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz#d4dd37b05520f1d996a384eb469320c2ada8377c"