diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte index 870e7cc..35afb4e 100644 --- a/src/lib/components/app-sidebar.svelte +++ b/src/lib/components/app-sidebar.svelte @@ -14,13 +14,12 @@ import Button, { buttonVariants } from './ui/button/button.svelte'; import User from './extra/User.svelte'; import type { SessionValidationResult } from '$lib/server/auth'; - import { Status, type OverviewUser } from '$lib'; + import { Status, type AppPSD, type OverviewUser } from '$lib'; import Label from './ui/label/label.svelte'; import { toast } from 'svelte-sonner'; import { enhance } from '$app/forms'; import { invalidateAll } from '$app/navigation'; import { fill_overview_data } from '$lib/state.svelte'; - import type { PageServerLoad } from '../../routes/app/$types'; import { overview_data } from '$lib/state.svelte'; @@ -33,7 +32,7 @@ }: { currentPage: string | null; subPage: string | null; - psd: PageServerLoad; + psd: AppPSD; user: SessionValidationResult['user']; } = $props(); @@ -219,10 +218,10 @@ {/if} - {#if data.friends.length === 0} + {#if overview_data.friends.length === 0}

You have no friends added.

{:else} - {#each data.friends as friend (friend.id)} + {#each overview_data.friends as friend (friend.id)} {friend.username} @@ -492,7 +491,7 @@ }} > {server.name} diff --git a/src/lib/components/extra/User.svelte b/src/lib/components/extra/User.svelte index d2a1242..2f7186a 100644 --- a/src/lib/components/extra/User.svelte +++ b/src/lib/components/extra/User.svelte @@ -14,7 +14,7 @@
{user.username} diff --git a/src/lib/components/member-sidebar.svelte b/src/lib/components/member-sidebar.svelte index 4a931a4..9157a8d 100644 --- a/src/lib/components/member-sidebar.svelte +++ b/src/lib/components/member-sidebar.svelte @@ -8,7 +8,7 @@ import type { SessionValidationResult } from '$lib/server/auth'; import { GroupID, - type OverviewData, + type AppPSD, type OverviewGroup, type OverviewServer, type OverviewUser @@ -19,7 +19,6 @@ import { invalidateAll } from '$app/navigation'; import { fill_overview_data } from '$lib/state.svelte'; import { toast } from 'svelte-sonner'; - import type { PageServerLoad } from '../../routes/app/$types'; import { overview_data } from '$lib/state.svelte'; @@ -34,7 +33,7 @@ }: { open: boolean; members: OverviewUser[]; - psd: PageServerLoad; + psd: AppPSD; user: SessionValidationResult['user']; currentEntity: OverviewGroup | OverviewServer; currentEntityId: string | null; diff --git a/src/lib/index.ts b/src/lib/index.ts index 79b3e7b..5ef1141 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -21,6 +21,48 @@ export interface Status { statusMessage: string; status: 1 | 2 | 3; } +export type AppPSD = { + user: { + servers: { + id: string; + name: string; + ownerId: string; + channels: unknown; + invites: unknown; + }[]; + friends: { + dmId: string | undefined; + id: string; + username: string; + }[]; + groups: { + members: number; + permissions: { + changeTitle: boolean; + addMembers: boolean; + removeMembers: boolean; + }; + id: string; + name: string; + ownerId: string; + changeTitle: number; + addMembers: number; + removeMembers: number; + }[]; + friendRequests: { + id: string; + fromUser: string; + fromUsername: string; + toUsername: string; + toUser: string; + }[]; + statusMessage: string; + id: string; + username: string; + statusOverwrite: number; + }; +}; + export const statuses: Map = new SvelteMap(); export const Status: Record = { @@ -40,6 +82,19 @@ export interface Channel { id: string; name: string; } + +export interface Invite { + code: 'πŸŽˆπŸπŸŽπŸ˜ΊπŸ™•'; + createdAt: Date; + createdBy: { + username: string; + id: string; + }; + id: string; + maxUses: number | null; + + uses: number; +} export interface Message { id: string; authorId: string; @@ -60,6 +115,7 @@ export type OverviewServer = { ownerId: string; image: string; channels: Channel[]; + invites: Invite[]; }; export type OverviewGroup = { id: string; diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index 8cd381e..212ff24 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -90,7 +90,8 @@ export async function validateSessionToken(token: string) { id: table.server.id, name: table.server.name, ownerId: table.server.owner, - channels: table.server.channels + channels: table.server.channels, + invites: table.server.invites }) .from(table.server) .where(inArray(table.server.id, user.servers as string[])) @@ -104,6 +105,7 @@ export async function validateSessionToken(token: string) { await Promise.all( (z.channels as string[]).map(async (m) => { const channel = await db.select().from(table.channel).where(eq(table.channel.id, m)); + //@TODO check if user can view channel if (!channel || channel.length == 0) return; return { name: channel[0].name, @@ -111,6 +113,32 @@ export async function validateSessionToken(token: string) { }; }) ) + ).filter(Boolean), + //@TODO check if user can view all invites (limit only to user's own invites if you can't) + invites: ( + await Promise.all( + (z.invites as string[]).map(async (m) => { + const invite = await db.select().from(table.invite).where(eq(table.invite.id, m)); + if (!invite || invite.length == 0) return; + return { + maxUses: invite[0].maxUses, + id: invite[0].id, + code: invite[0].code, + uses: (invite[0].uses as string[]).length, + createdAt: new Date(invite[0].createdAt), + createdBy: await db + .select() + .from(table.user) + .where(eq(table.user.id, invite[0].creatorId)) + .then((user) => { + return { + username: user[0].username, + id: user[0].id + }; + }) + }; + }) + ) ).filter(Boolean) }; }) diff --git a/src/lib/state.svelte.ts b/src/lib/state.svelte.ts index 334ed65..a7bbcaf 100644 --- a/src/lib/state.svelte.ts +++ b/src/lib/state.svelte.ts @@ -1,4 +1,12 @@ -import { GroupID, ServerID, statuses, UserID, type Channel, type OverviewData } from '$lib'; +import { + GroupID, + ServerID, + statuses, + UserID, + type Channel, + type Invite, + type OverviewData +} from '$lib'; import type { PageServerData } from '../routes/app/$types'; export const overview_data: OverviewData = $state({ @@ -14,7 +22,8 @@ export async function fill_overview_data(data: PageServerData) { name: z.name, ownerId: z.ownerId, channels: z.channels as Channel[], - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name + invites: z.invites as Invite[], + image: 'https://api.dicebear.com/9.x/glass/svg?seed=' + z.name }; }); overview_data.groups = data.user.groups.map((z) => { @@ -24,7 +33,7 @@ export async function fill_overview_data(data: PageServerData) { ownerId: z.ownerId, members: z.members, permissions: z.permissions, - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name + image: 'https://api.dicebear.com/9.x/glass/svg?seed=' + z.name }; }); overview_data.friends = await Promise.all( @@ -46,7 +55,7 @@ export async function fill_overview_data(data: PageServerData) { id: UserID.parse(friend.id), username: friend.username, dmId: friend.dmId, - image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username + image: 'https://api.dicebear.com/9.x/glass/svg/svg?seed=' + friend.username }; }) ); diff --git a/src/routes/api/members/[entityId]/+server.ts b/src/routes/api/members/[entityId]/+server.ts index fa0638d..ac5f146 100644 --- a/src/routes/api/members/[entityId]/+server.ts +++ b/src/routes/api/members/[entityId]/+server.ts @@ -37,7 +37,7 @@ export const GET: RequestHandler = async ({ params }) => { ? kvStore.get('user-' + member.id + '-message') : '', username: member.username, - image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` + image: `https://api.dicebear.com/9.x/glass/svg?seed=${member.username}` })) }); } @@ -67,7 +67,7 @@ export const GET: RequestHandler = async ({ params }) => { ? kvStore.get('user-' + member.id + '-message') : '', username: member.username, - image: `https://api.dicebear.com/7.x/pixel-art/svg?seed=${member.username}` + image: `https://api.dicebear.com/9.x/glass/svg?seed=${member.username}` })) }); } diff --git a/src/routes/app/+page.svelte b/src/routes/app/+page.svelte index 2057326..53f7300 100644 --- a/src/routes/app/+page.svelte +++ b/src/routes/app/+page.svelte @@ -1,6 +1,5 @@ @@ -347,205 +267,11 @@ {#if currentPageID && currentPage && ServerID.is(currentPageID) && !currentSubPageID} - {@const server = currentPage as OverviewServer} - - - - Roles - General - - -
-
-

Roles (fake)

-
- - -
-
- -
- {#each fakeRoles as role (role.id)} - - {/if} - - {/each} -
- - {#if selectedRole} - - - {selectedRole.name} permissions - - -
- {#each Object.entries(selectedRole.permissions) as [perm, enabled] (perm)} -
- - { - selectedRole.permissions[perm as FakePermission] = e.detail; - }} - /> -
- {/each} -
-

- Changes are local only (no backend yet) -

-
-
- {/if} -
-
- -
-

Server Settings

-
- { - server.name = e.currentTarget.value; - }} - /> - -
-

Changes are local only (no backend yet)

-
- -
-

Invites

-
{ - return async ({ result }) => { - if (result.type === 'success') { - toast.success('Invite created successfully'); - inviteCode = location.origin + '/invite/' + result.data!.code; - await invalidateAll(); - await fill_overview_data(data); - } else if (result.type === 'failure') { - toast.error('Failed to create invite: ' + result.data?.error); - } - }; - }} - class="flex gap-2" - > - -
- -
- - -
-
- -
- -
-

Active Invites

-
- {#each server.invites || [] as invite (invite.code)} -
-
-
{invite.code}
-
- {invite.uses}/{invite.maxUses || '∞'} uses β€’ Created by {invite.creator} on {formatTimestamp( - invite.createdAt - )} -
-
- -
- {/each} -
-
-
-
-
+ {/if} {#if currentPageID && currentPage && ((ServerID.is(currentPageID) && ChannelID.is(currentSubPageID)) || UserID.is(currentPageID) || GroupID.is(currentPageID))}
@@ -553,7 +279,7 @@ {#if i === 0 || messages[i - 1].authorId !== message.authorId}
{message.author.name} diff --git a/src/routes/app/ServerDashboard.svelte b/src/routes/app/ServerDashboard.svelte new file mode 100644 index 0000000..189a376 --- /dev/null +++ b/src/routes/app/ServerDashboard.svelte @@ -0,0 +1,331 @@ + + + + + Roles + General + Dangerous + + +
+

Danger Zone

+

+ These actions are irreversible and will affect all members of the server. +

+ +
{ + return async ({ result }) => { + if (result.type === 'success') { + toast.success('Server deleted successfully'); + await invalidateAll(); + await fill_overview_data(psd); + } else if (result.type === 'failure') { + toast.error('Failed to delete server: ' + result.data?.error); + } + }; + }} + class="flex items-center gap-2 pt-2" + > + + +

+ This will permanently delete the server and all its data. +

+
+
+
+ +
+
+

Roles (fake)

+
+ + +
+
+ +
+ {#each fakeRoles as role (role.id)} + + {/if} + + {/each} +
+ + {#if selectedRole} + + + {selectedRole.name} permissions + + +
+ {#each Object.entries(selectedRole.permissions) as [perm, enabled] (perm)} +
+ + { + selectedRole.permissions[perm as FakePermission] = e.detail; + }} + /> +
+ {/each} +
+

Changes are local only (no backend yet)

+
+
+ {/if} +
+
+ +
+

Server Settings

+
+ { + server.name = e.currentTarget.value; + }} + /> + +
+

Changes are local only (no backend yet)

+
+ +
+

Invites

+
{ + return async ({ result }) => { + if (result.type === 'success') { + toast.success('Invite created successfully'); + inviteCode = location.origin + '/invite/' + result.data!.code; + await invalidateAll(); + await fill_overview_data(psd); + } else if (result.type === 'failure') { + toast.error('Failed to create invite: ' + result.data?.error); + } + }; + }} + class="flex gap-2" + > + +
+ +
+ + +
+
+ +
+ +
+

Active Invites

+
+ {#each server.invites || [] as invite (invite.code)} +
+
+
{invite.code}
+
+ {invite.uses}/{invite.maxUses || '∞'} uses β€’ Created by {invite.createdBy + .username} on {formatTimestamp(invite.createdAt)} +
+
+
{ + return async ({ result }) => { + if (result.type === 'success') { + toast.success('Invite deleted successfully'); + await invalidateAll(); + await fill_overview_data(psd); + } else if (result.type === 'failure') { + toast.error('Failed to delete invite: ' + result.data?.error); + } + }; + }} + class="m-0" + > + + + +
+
+ {/each} +
+
+
+
+
diff --git a/src/routes/app/actions/server.ts b/src/routes/app/actions/server.ts index 46ef054..e4d2edf 100644 --- a/src/routes/app/actions/server.ts +++ b/src/routes/app/actions/server.ts @@ -120,7 +120,7 @@ export default { code, creatorId: locals.user!.id, createdAt: new Date(), - maxUses: maxUses <= 0 ? undefined : maxUses // if maxUses is not undefined, there are infnite uses + maxUses: maxUses <= 0 ? undefined : maxUses // if maxUses is not undefined, there are infinite uses }); await tx @@ -132,6 +132,45 @@ export default { }); return { success: true, code }; }, + deleteInvite: async ({ request, locals }) => { + const data = await request.formData(); + const inviteCode = data.get('inviteCode'); + const serverId = data.get('serverId'); + + if (typeof inviteCode !== 'string' || typeof serverId !== 'string') { + return fail(400, { error: 'Invalid invite data' }); + } + + const server = await db.select().from(table.server).where(eq(table.server.id, serverId)); + if (!server || server.length == 0) { + return fail(400, { error: 'Server ID invalid' }); + } + + const invRaw = await db + .select() + .from(table.invite) + .where(eq(table.invite.code, inviteCode)) + .limit(1); + const invite = invRaw[0]; + + if (!invite) { + return fail(400, { error: 'Invite not found' }); + } + + // @TODO check permissions here (if owner ignore, then check roles, if any role has manageInvites then let delete invite) + await db.transaction(async (tx) => { + await tx + .update(table.server) + .set({ + invites: (server[0].invites as string[]).filter((id) => id !== invite.id) + }) + .where(eq(table.server.id, serverId)); + + await tx.delete(table.invite).where(eq(table.invite.id, invite.id)); + }); + + return { success: true }; + }, createServer: async ({ request, locals }) => { const data = await request.formData(); const name = data.get('name'); @@ -152,5 +191,47 @@ export default { _sendToUser(locals.user!.id, { type: 'server', status: 'server-created' }); return { success: true }; + }, + deleteServer: async ({ request, locals }) => { + const data = await request.formData(); + const serverId = data.get('serverId'); + + if (typeof serverId !== 'string') { + return fail(400, { error: 'Server ID incorrect' }); + } + + const server = await db.select().from(table.server).where(eq(table.server.id, serverId)); + if (!server || server.length == 0) { + return fail(400, { error: 'Server ID invalid' }); + } + + // @TODO check permissions here (only owner should be able to delete server) + + // Remove server from all members' server lists + await db.transaction(async (tx) => { + for (const memberId of server[0].members as string[]) { + await tx + .update(table.user) + .set({ + servers: ( + ( + await tx + .select({ servers: table.user.servers }) + .from(table.user) + .where(eq(table.user.id, memberId)) + )[0].servers as string[] + ).filter((id: string) => id !== serverId) + }) + .where(eq(table.user.id, memberId)); + } + + await tx.delete(table.invite).where(eq(table.invite.serverId, serverId)); + + await tx.delete(table.channel).where(eq(table.channel.serverId, serverId)); + + await tx.delete(table.server).where(eq(table.server.id, serverId)); + }); + _sendToUser(locals.user!.id, { type: 'server', status: 'server-deleted' }); + return { success: true }; } } satisfies Actions;