Implement most of status system (still kinda bootycheeks at checking if
you're connected), fionally fix prettierrc
This commit is contained in:
parent
126acf52f3
commit
e64910d895
29 changed files with 2001 additions and 879 deletions
|
|
@ -1,27 +1,81 @@
|
|||
import type { Handle } from '@sveltejs/kit';
|
||||
import * as auth from '$lib/server/auth';
|
||||
import { kvStore } from '$lib/server/db';
|
||||
import { _sendToSubscribers } from './routes/api/updates/+server';
|
||||
import { Status } from '$lib';
|
||||
|
||||
const handleAuth: Handle = async ({ event, resolve }) => {
|
||||
const sessionToken = event.cookies.get(auth.sessionCookieName);
|
||||
const sessionToken = event.cookies.get(auth.sessionCookieName);
|
||||
|
||||
if (!sessionToken) {
|
||||
event.locals.user = null;
|
||||
event.locals.session = null;
|
||||
if (!sessionToken) {
|
||||
event.locals.user = null;
|
||||
event.locals.session = null;
|
||||
|
||||
return resolve(event);
|
||||
}
|
||||
return resolve(event);
|
||||
}
|
||||
|
||||
const { session, user } = await auth.validateSessionToken(sessionToken);
|
||||
if (session) {
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
} else {
|
||||
auth.deleteSessionTokenCookie(event);
|
||||
}
|
||||
const { session, user } = await auth.validateSessionToken(sessionToken);
|
||||
if (session) {
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
} else {
|
||||
auth.deleteSessionTokenCookie(event);
|
||||
}
|
||||
|
||||
event.locals.user = user;
|
||||
event.locals.session = session;
|
||||
event.locals.user = user;
|
||||
event.locals.session = session;
|
||||
|
||||
return resolve(event);
|
||||
if (event.locals.user) {
|
||||
const id = event.locals.user.id;
|
||||
const overwrite = event.locals.user.statusOverwrite as 1 | 2 | 3 | undefined;
|
||||
const now = Date.now();
|
||||
|
||||
const lastActiveKey = `user-${id}-last-active`;
|
||||
const stateKey = `user-${id}-state`;
|
||||
const timerKey = `user-${id}-offline-timer`;
|
||||
|
||||
const lastActive = kvStore.get<number>(lastActiveKey) ?? 0;
|
||||
const state = kvStore.get<1 | 2 | 3>(stateKey) ?? Status.OFFLINE;
|
||||
|
||||
// always update activity
|
||||
kvStore.set(lastActiveKey, now);
|
||||
if (overwrite !== Status.OFFLINE) {
|
||||
const shouldSend = state === Status.OFFLINE && now - lastActive > 5000;
|
||||
|
||||
if (shouldSend) {
|
||||
const outgoingStatus = overwrite === Status.DND ? Status.DND : Status.ONLINE;
|
||||
|
||||
_sendToSubscribers(id, {
|
||||
type: 'status',
|
||||
id,
|
||||
status: outgoingStatus
|
||||
});
|
||||
|
||||
kvStore.set(stateKey, outgoingStatus);
|
||||
}
|
||||
}
|
||||
const existingTimer = kvStore.get<NodeJS.Timeout>(timerKey);
|
||||
if (existingTimer) {
|
||||
clearTimeout(existingTimer);
|
||||
}
|
||||
|
||||
const offlineTimer = setTimeout(() => {
|
||||
const last = kvStore.get<number>(lastActiveKey) ?? 0;
|
||||
|
||||
if (Date.now() - last >= 10_000) {
|
||||
_sendToSubscribers(id, {
|
||||
type: 'status',
|
||||
id,
|
||||
status: Status.OFFLINE
|
||||
});
|
||||
|
||||
kvStore.set(stateKey, Status.OFFLINE);
|
||||
kvStore.delete(timerKey);
|
||||
}
|
||||
}, 10_000);
|
||||
|
||||
kvStore.set(timerKey, offlineTimer + '');
|
||||
}
|
||||
return resolve(event);
|
||||
};
|
||||
|
||||
export const handle: Handle = handleAuth;
|
||||
|
|
|
|||
|
|
@ -1,22 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { type Data, type OverviewUser } from "$lib";
|
||||
import * as Collapsible from "$lib/components/ui/collapsible/index.js";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import * as Dialog from "$lib/components/ui/dialog/index.js";
|
||||
import * as Tabs from "$lib/components/ui/tabs/index.js";
|
||||
import * as Card from "$lib/components/ui/card/index.js";
|
||||
import MessagesSquare from "@lucide/svelte/icons/messages-square";
|
||||
import MinusIcon from "@lucide/svelte/icons/minus";
|
||||
import PlusIcon from "@lucide/svelte/icons/plus";
|
||||
import { type Data, type OverviewUser } from '$lib';
|
||||
import * as Collapsible from '$lib/components/ui/collapsible/index.js';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import MessagesSquare from '@lucide/svelte/icons/messages-square';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import PlusIcon from '@lucide/svelte/icons/plus';
|
||||
import UserRoundPlus from '@lucide/svelte/icons/user-round-plus';
|
||||
import UsersRound from '@lucide/svelte/icons/users-round';
|
||||
import CirclePlus from '@lucide/svelte/icons/circle-plus';
|
||||
import Input from "./ui/input/input.svelte";
|
||||
import Button, { buttonVariants } from "./ui/button/button.svelte";
|
||||
import User from "./extra/User.svelte";
|
||||
import type { SessionValidationResult } from "$lib/server/auth";
|
||||
import Input from './ui/input/input.svelte';
|
||||
import Button, { buttonVariants } from './ui/button/button.svelte';
|
||||
import User from './extra/User.svelte';
|
||||
import type { SessionValidationResult } from '$lib/server/auth';
|
||||
|
||||
let { currentPage = $bindable<string|null>(), data, user, ...restProps }: {currentPage: string|null, data: Data, user: SessionValidationResult['user'] }= $props();
|
||||
let {
|
||||
currentPage = $bindable<string | null>(),
|
||||
data,
|
||||
user,
|
||||
...restProps
|
||||
}: { currentPage: string | null; data: Data; user: SessionValidationResult['user'] } = $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root {...restProps}>
|
||||
|
|
@ -25,7 +30,7 @@
|
|||
<Sidebar.MenuItem>
|
||||
<Sidebar.MenuButton size="lg">
|
||||
<div
|
||||
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
|
||||
class="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground"
|
||||
>
|
||||
<MessagesSquare class="size-4" />
|
||||
</div>
|
||||
|
|
@ -33,193 +38,170 @@
|
|||
<span class="font-medium">chat.sad.ovh</span>
|
||||
</div>
|
||||
</Sidebar.MenuButton>
|
||||
<div class="w-full flex gap-2 justify-center">
|
||||
<div class="flex w-full justify-center gap-2">
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button
|
||||
variant={user!.friendRequests.length > 0 ? 'destructive' : 'outline'}
|
||||
size="icon"
|
||||
>
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant={user!.friendRequests.length > 0 ? "destructive" : "outline"} size="icon">
|
||||
<UserRoundPlus />
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username or manage pending requests.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<!-- input to add a new friend -->
|
||||
<form method="POST" action="?/addFriend" class="mb-4">
|
||||
<Input name="username" placeholder="username" required class="mb-2" />
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Send request</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
|
||||
<!-- Tabs for Friend Requests -->
|
||||
<Tabs.Root value="outgoing">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username or manage pending requests.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<!-- Outgoing Requests -->
|
||||
<Tabs.Content value="outgoing">
|
||||
{#if user.friendRequests.filter((r) => r.fromUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No outgoing requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter((r) => r.fromUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Request sent</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Cancel</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
|
||||
<!-- input to add a new friend -->
|
||||
<form method="POST" action="?/addFriend" class="mb-4">
|
||||
<Input name="username" placeholder="username" required class="mb-2" />
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Send request</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
<!-- Incoming Requests -->
|
||||
<Tabs.Content value="incoming">
|
||||
{#if user.friendRequests.filter((r) => r.toUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No incoming requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter((r) => r.toUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Sent you a friend request</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex gap-2">
|
||||
<!-- accept friend -->
|
||||
<form method="POST" action="?/addFriend">
|
||||
<input type="hidden" name="userId" value={request.fromUser} />
|
||||
<Button type="submit" size="sm">Accept</Button>
|
||||
</form>
|
||||
<!-- decline friend -->
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Decline</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Tabs for Friend Requests -->
|
||||
<Tabs.Root value="outgoing">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="outgoing">Outgoing</Tabs.Trigger>
|
||||
<Tabs.Trigger value="incoming">Incoming</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<!-- Outgoing Requests -->
|
||||
<Tabs.Content value="outgoing">
|
||||
{#if user.friendRequests.filter(r => r.fromUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No outgoing requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter(r => r.fromUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Request sent</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer>
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">
|
||||
Cancel
|
||||
</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createGroup">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a group</Dialog.Title>
|
||||
<Dialog.Description>Add friends into your group!</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<!-- Incoming Requests -->
|
||||
<Tabs.Content value="incoming">
|
||||
{#if user.friendRequests.filter(r => r.toUser === user.id).length === 0}
|
||||
<p class="text-sm text-muted-foreground">No incoming requests</p>
|
||||
{:else}
|
||||
{#each user.friendRequests.filter(r => r.toUser === user.id) as request (request.id)}
|
||||
<Card.Root class="mb-2">
|
||||
<Card.Header>
|
||||
<Card.Title>{request.username}</Card.Title>
|
||||
<Card.Description>Sent you a friend request</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Footer class="flex gap-2">
|
||||
<!-- accept friend -->
|
||||
<form method="POST" action="?/addFriend">
|
||||
<input type="hidden" name="userId" value={request.fromUser} />
|
||||
<Button type="submit" size="sm">Accept</Button>
|
||||
</form>
|
||||
<!-- decline friend -->
|
||||
<form method="POST" action="?/cancelFriendRequest">
|
||||
<input type="hidden" name="requestId" value={request.id} />
|
||||
<Button type="submit" variant="outline" size="sm">Decline</Button>
|
||||
</form>
|
||||
</Card.Footer>
|
||||
</Card.Root>
|
||||
{/each}
|
||||
{/if}
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
</Dialog.Content>
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<label class="flex items-center gap-2">
|
||||
<input type="checkbox" name="member" value={friend.id} />
|
||||
<User user={friend} />
|
||||
</label>
|
||||
{/each}
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
</Dialog.Root>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/joinServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Join a server</Dialog.Title>
|
||||
<Dialog.Description>Enter an invite link.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createGroup">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a group</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add friends into your group!
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Input name="invite" placeholder="invite link" required />
|
||||
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<label class="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="member"
|
||||
value={friend.id}
|
||||
/>
|
||||
<User user={friend} />
|
||||
</label>
|
||||
{/each}
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Join</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a server</Dialog.Title>
|
||||
<Dialog.Description>Name your new server.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/joinServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Join a server</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Enter an invite link.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Input name="invite" placeholder="invite link" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Join</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/createServer">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a server</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Name your new server.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Input name="name" placeholder="Server name" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Create</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
<Input name="name" placeholder="Server name" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: 'outline' })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
</div>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
|
|
@ -230,26 +212,24 @@
|
|||
<Collapsible.Root open={true} class="group/collapsible">
|
||||
<Sidebar.MenuItem>
|
||||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Friends
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
</Sidebar.MenuButton>
|
||||
<Sidebar.MenuButton>
|
||||
Friends
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<Sidebar.MenuSub>
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
|
||||
<User onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}} user={friend}></User>
|
||||
<User
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}}
|
||||
user={friend}
|
||||
></User>
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
|
|
@ -263,12 +243,8 @@
|
|||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Groups
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
|
|
@ -276,10 +252,13 @@
|
|||
{#each data.groups as group (group.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
<a onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = group.id;
|
||||
}} href="##">
|
||||
<a
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = group.id;
|
||||
}}
|
||||
href="##"
|
||||
>
|
||||
{group.name} ({group.members} members)
|
||||
</a>
|
||||
</Sidebar.MenuSubButton>
|
||||
|
|
@ -295,12 +274,8 @@
|
|||
<Collapsible.Trigger>
|
||||
<Sidebar.MenuButton>
|
||||
Servers
|
||||
<PlusIcon
|
||||
class="ms-auto group-data-[state=open]/collapsible:hidden"
|
||||
/>
|
||||
<MinusIcon
|
||||
class="ms-auto group-data-[state=closed]/collapsible:hidden"
|
||||
/>
|
||||
<PlusIcon class="ms-auto group-data-[state=open]/collapsible:hidden" />
|
||||
<MinusIcon class="ms-auto group-data-[state=closed]/collapsible:hidden" />
|
||||
</Sidebar.MenuButton>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
|
|
@ -308,11 +283,19 @@
|
|||
{#each data.servers as server (server.id)}
|
||||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
<a onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = server.id;
|
||||
}} href="##" class="flex items-center gap-2">
|
||||
<img src={"https://api.dicebear.com/7.x/pixel-art/svg?seed=" + server.name} alt={server.name} class="size-6 rounded-full" />
|
||||
<a
|
||||
onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = server.id;
|
||||
}}
|
||||
href="##"
|
||||
class="flex items-center gap-2"
|
||||
>
|
||||
<img
|
||||
src={'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + server.name}
|
||||
alt={server.name}
|
||||
class="size-6 rounded-full"
|
||||
/>
|
||||
{server.name}
|
||||
</a>
|
||||
</Sidebar.MenuSubButton>
|
||||
|
|
|
|||
9
src/lib/components/hooks/is-mobile.svelte.ts
Normal file
9
src/lib/components/hooks/is-mobile.svelte.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import { MediaQuery } from "svelte/reactivity";
|
||||
|
||||
const DEFAULT_MOBILE_BREAKPOINT = 768;
|
||||
|
||||
export class IsMobile extends MediaQuery {
|
||||
constructor(breakpoint: number = DEFAULT_MOBILE_BREAKPOINT) {
|
||||
super(`max-width: ${breakpoint - 1}px`);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,48 +1,48 @@
|
|||
import { definePrefix, type Puuid } from "./puuid"
|
||||
import { definePrefix, type Puuid } from './puuid';
|
||||
|
||||
export const UserID = definePrefix("user");
|
||||
export const GroupID = definePrefix("group");
|
||||
export const ServerID = definePrefix("srv");
|
||||
export const FriendRequestID = definePrefix("frq");
|
||||
export const UserID = definePrefix('user');
|
||||
export const GroupID = definePrefix('group');
|
||||
export const ServerID = definePrefix('srv');
|
||||
export const FriendRequestID = definePrefix('frq');
|
||||
|
||||
export type UserId = Puuid<"user">;
|
||||
export type GroupId = Puuid<"group">;
|
||||
export type ServerId = Puuid<"srv">;
|
||||
export type FriendRequestID = Puuid<"frq">;
|
||||
export type UserId = Puuid<'user'>;
|
||||
export type GroupId = Puuid<'group'>;
|
||||
export type ServerId = Puuid<'srv'>;
|
||||
export type FriendRequestID = Puuid<'frq'>;
|
||||
|
||||
export const Status: Record<string, 1|2|3> = {
|
||||
export const Status: Record<string, 1 | 2 | 3> = {
|
||||
OFFLINE: 1,
|
||||
DND: 2,
|
||||
ONLINE: 3
|
||||
}
|
||||
};
|
||||
|
||||
export type OverviewUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
image: string;
|
||||
id: string;
|
||||
username: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type OverviewServer = {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
image: string;
|
||||
};
|
||||
export type OverviewGroup = {
|
||||
id: string;
|
||||
name: string;
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
members: number;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export interface OverviewData {
|
||||
friends: OverviewUser[],
|
||||
groups: OverviewGroup[],
|
||||
servers: OverviewServer[],
|
||||
};
|
||||
|
||||
export interface UserWithStatus extends OverviewUser {
|
||||
status: 1|2|3,
|
||||
statusMessage: string
|
||||
status: 1 | 2 | 3;
|
||||
statusMessage: string;
|
||||
}
|
||||
|
||||
export interface OverviewData {
|
||||
friends: UserWithStatus[];
|
||||
groups: OverviewGroup[];
|
||||
servers: OverviewServer[];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,36 @@
|
|||
import { v4 as uuidv4 } from "uuid";
|
||||
import { v7 as uuidv7 } from "uuid";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { v7 as uuidv7 } from 'uuid';
|
||||
|
||||
type Brand<K, T> = K & { __brand: T };
|
||||
|
||||
export type Puuid<Prefix extends string> = Brand<
|
||||
string,
|
||||
{ prefix: Prefix }
|
||||
>;
|
||||
export type Puuid<Prefix extends string> = Brand<string, { prefix: Prefix }>;
|
||||
|
||||
export function definePrefix<const P extends string>(prefix: P) {
|
||||
const withPrefix = (uuid: string) =>
|
||||
`${prefix}_${uuid}` as Puuid<P>;
|
||||
const withPrefix = (uuid: string) => `${prefix}_${uuid}` as Puuid<P>;
|
||||
|
||||
return {
|
||||
prefix,
|
||||
is(value: string): value is Puuid<P> {
|
||||
return value.startsWith(prefix + "_");
|
||||
},
|
||||
return {
|
||||
prefix,
|
||||
is(value: string): value is Puuid<P> {
|
||||
return value.startsWith(prefix + '_');
|
||||
},
|
||||
|
||||
newV7(): Puuid<P> {
|
||||
return withPrefix(uuidv7());
|
||||
},
|
||||
newV7(): Puuid<P> {
|
||||
return withPrefix(uuidv7());
|
||||
},
|
||||
|
||||
newV4(): Puuid<P> {
|
||||
return withPrefix(uuidv4());
|
||||
},
|
||||
newV4(): Puuid<P> {
|
||||
return withPrefix(uuidv4());
|
||||
},
|
||||
|
||||
parse(value: string): Puuid<P> {
|
||||
if (!value.startsWith(prefix + "_")) {
|
||||
throw new Error(
|
||||
`Invalid prefix, expected "${prefix}_"`
|
||||
);
|
||||
}
|
||||
return value as Puuid<P>;
|
||||
},
|
||||
parse(value: string): Puuid<P> {
|
||||
if (!value.startsWith(prefix + '_')) {
|
||||
throw new Error(`Invalid prefix, expected "${prefix}_"`);
|
||||
}
|
||||
return value as Puuid<P>;
|
||||
},
|
||||
|
||||
inner(id: Puuid<P>): string {
|
||||
return id.slice(prefix.length + 1);
|
||||
},
|
||||
};
|
||||
inner(id: Puuid<P>): string {
|
||||
return id.slice(prefix.length + 1);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,141 +10,151 @@ const DAY_IN_MS = 1000 * 60 * 60 * 24;
|
|||
export const sessionCookieName = 'auth-session';
|
||||
|
||||
export function generateSessionToken() {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(18));
|
||||
const token = encodeBase64url(bytes);
|
||||
return token;
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(18));
|
||||
const token = encodeBase64url(bytes);
|
||||
return token;
|
||||
}
|
||||
|
||||
export async function createSession(token: string, userId: string) {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const session: table.Session = {
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
|
||||
};
|
||||
await db.insert(table.session).values(session);
|
||||
return session;
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const session: table.Session = {
|
||||
id: sessionId,
|
||||
userId,
|
||||
expiresAt: new Date(Date.now() + DAY_IN_MS * 30)
|
||||
};
|
||||
await db.insert(table.session).values(session);
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function validateSessionToken(token: string) {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const [row] = await db
|
||||
.select({
|
||||
user: {
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
friends: table.user.friends,
|
||||
servers: table.user.servers,
|
||||
groups: table.user.groups,
|
||||
},
|
||||
session: table.session,
|
||||
})
|
||||
.from(table.session)
|
||||
.innerJoin(table.user, eq(table.session.userId, table.user.id))
|
||||
.where(eq(table.session.id, sessionId));
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const [row] = await db
|
||||
.select({
|
||||
user: {
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
friends: table.user.friends,
|
||||
servers: table.user.servers,
|
||||
groups: table.user.groups,
|
||||
statusOverwrite: table.user.statusOverwrite
|
||||
},
|
||||
session: table.session
|
||||
})
|
||||
.from(table.session)
|
||||
.innerJoin(table.user, eq(table.session.userId, table.user.id))
|
||||
.where(eq(table.session.id, sessionId));
|
||||
|
||||
if (!row) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const { session, user } = row;
|
||||
if (!row) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const { session, user } = row;
|
||||
|
||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||
if (sessionExpired) {
|
||||
await db.delete(table.session).where(eq(table.session.id, session.id));
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||
if (sessionExpired) {
|
||||
await db.delete(table.session).where(eq(table.session.id, session.id));
|
||||
return { session: null, user: null };
|
||||
}
|
||||
|
||||
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
|
||||
if (renewSession) {
|
||||
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
||||
await db
|
||||
.update(table.session)
|
||||
.set({ expiresAt: session.expiresAt })
|
||||
.where(eq(table.session.id, session.id));
|
||||
}
|
||||
const friends = (user.friends as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.user.id,
|
||||
username: table.user.username,
|
||||
})
|
||||
.from(table.user)
|
||||
.where(inArray(table.user.id, (user.friends as string[])))
|
||||
: [];
|
||||
const renewSession = Date.now() >= session.expiresAt.getTime() - DAY_IN_MS * 15;
|
||||
if (renewSession) {
|
||||
session.expiresAt = new Date(Date.now() + DAY_IN_MS * 30);
|
||||
await db
|
||||
.update(table.session)
|
||||
.set({ expiresAt: session.expiresAt })
|
||||
.where(eq(table.session.id, session.id));
|
||||
}
|
||||
const friends = (user.friends as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.user.id,
|
||||
username: table.user.username
|
||||
})
|
||||
.from(table.user)
|
||||
.where(inArray(table.user.id, user.friends as string[]))
|
||||
: [];
|
||||
|
||||
const servers = (user.servers as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.server.id,
|
||||
name: table.server.name,
|
||||
ownerId: table.server.owner,
|
||||
})
|
||||
.from(table.server)
|
||||
.where(inArray(table.server.id, (user.servers as string[])))
|
||||
: [];
|
||||
const groups = (user.groups as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.group.id,
|
||||
name: table.group.name,
|
||||
ownerId: table.group.owner,
|
||||
members: table.group.members
|
||||
})
|
||||
.from(table.group)
|
||||
.where(inArray(table.group.id, (user.groups as string[])))
|
||||
: [];
|
||||
const servers = (user.servers as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.server.id,
|
||||
name: table.server.name,
|
||||
ownerId: table.server.owner
|
||||
})
|
||||
.from(table.server)
|
||||
.where(inArray(table.server.id, user.servers as string[]))
|
||||
: [];
|
||||
const groups = (user.groups as string[]).length
|
||||
? await db
|
||||
.select({
|
||||
id: table.group.id,
|
||||
name: table.group.name,
|
||||
ownerId: table.group.owner,
|
||||
members: table.group.members
|
||||
})
|
||||
.from(table.group)
|
||||
.where(inArray(table.group.id, user.groups as string[]))
|
||||
: [];
|
||||
|
||||
const friendRequests = await db
|
||||
.select({
|
||||
id: table.friendRequest.id,
|
||||
fromUser: table.friendRequest.fromUser,
|
||||
toUser: table.friendRequest.toUser,
|
||||
})
|
||||
.from(table.friendRequest)
|
||||
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)))
|
||||
const friendRequests = await db
|
||||
.select({
|
||||
id: table.friendRequest.id,
|
||||
fromUser: table.friendRequest.fromUser,
|
||||
toUser: table.friendRequest.toUser
|
||||
})
|
||||
.from(table.friendRequest)
|
||||
.where(or(eq(table.friendRequest.fromUser, user.id), eq(table.friendRequest.toUser, user.id)));
|
||||
|
||||
|
||||
return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}}), friendRequests} };
|
||||
return {
|
||||
session,
|
||||
user: {
|
||||
...user,
|
||||
servers,
|
||||
friends,
|
||||
groups: groups.map((z) => {
|
||||
return { ...z, members: (z.members as string[]).length };
|
||||
}),
|
||||
friendRequests
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
|
||||
|
||||
export async function invalidateSession(sessionId: string) {
|
||||
await db.delete(table.session).where(eq(table.session.id, sessionId));
|
||||
await db.delete(table.session).where(eq(table.session.id, sessionId));
|
||||
}
|
||||
|
||||
export function setSessionTokenCookie(event: RequestEvent, token: string, expiresAt: Date) {
|
||||
event.cookies.set(sessionCookieName, token, {
|
||||
expires: expiresAt,
|
||||
path: '/'
|
||||
});
|
||||
event.cookies.set(sessionCookieName, token, {
|
||||
expires: expiresAt,
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteSessionTokenCookie(event: RequestEvent) {
|
||||
event.cookies.delete(sessionCookieName, {
|
||||
path: '/'
|
||||
});
|
||||
event.cookies.delete(sessionCookieName, {
|
||||
path: '/'
|
||||
});
|
||||
}
|
||||
|
||||
export function validateUsername(username: unknown): username is string {
|
||||
return (
|
||||
typeof username === 'string' &&
|
||||
username.length >= 3 &&
|
||||
username.length <= 31 &&
|
||||
/^[a-z0-9_-]+$/.test(username)
|
||||
);
|
||||
return (
|
||||
typeof username === 'string' &&
|
||||
username.length >= 3 &&
|
||||
username.length <= 31 &&
|
||||
/^[a-z0-9_-]+$/.test(username)
|
||||
);
|
||||
}
|
||||
|
||||
export function validatePassword(password: unknown): password is string {
|
||||
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
|
||||
return typeof password === 'string' && password.length >= 6 && password.length <= 255;
|
||||
}
|
||||
|
||||
|
||||
export function validateEmail(email: unknown): email is string {
|
||||
return (
|
||||
typeof email === 'string' &&
|
||||
email.length >= 5 &&
|
||||
email.length <= 254 &&
|
||||
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
|
||||
);
|
||||
return (
|
||||
typeof email === 'string' &&
|
||||
email.length >= 5 &&
|
||||
email.length <= 254 &&
|
||||
/^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$/i.test(email)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import { Database } from 'bun:sqlite';
|
||||
import { BunSqliteKeyValue } from "bun-sqlite-key-value"
|
||||
import { BunSqliteKeyValue } from 'bun-sqlite-key-value';
|
||||
|
||||
const sqlite = new Database('database.db');
|
||||
export const db = drizzle(sqlite);
|
||||
|
||||
export const kvStore = new BunSqliteKeyValue("./kvStore.db")
|
||||
export const kvStore = new BunSqliteKeyValue('./kvStore.db');
|
||||
|
|
|
|||
|
|
@ -1,14 +1,16 @@
|
|||
import { Status } from '../../index.ts';
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
|
||||
export const user = sqliteTable('user', {
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
friends: text('friends', { mode: "json"}).default([]).notNull(),
|
||||
id: text('id').primaryKey(),
|
||||
username: text('username').notNull().unique(),
|
||||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').notNull(),
|
||||
statusOverwrite: integer('status_overwrite').default(Status.ONLINE).notNull(),
|
||||
friends: text('friends', { mode: 'json' }).default([]).notNull(),
|
||||
|
||||
servers: text('servers', { mode: "json"}).default([]).notNull(), // string[] of ServerIDs
|
||||
groups: text('groups', { mode: "json"}).default([]).notNull(), // string[] of GroupIDs
|
||||
servers: text('servers', { mode: 'json' }).default([]).notNull(), // string[] of ServerIDs
|
||||
groups: text('groups', { mode: 'json' }).default([]).notNull() // string[] of GroupIDs
|
||||
});
|
||||
|
||||
export const session = sqliteTable('session', {
|
||||
|
|
@ -19,39 +21,66 @@ export const session = sqliteTable('session', {
|
|||
expiresAt: integer('expires_at', { mode: 'timestamp' }).notNull()
|
||||
});
|
||||
|
||||
export const server = sqliteTable("server", {
|
||||
export const server = sqliteTable('server', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
owner: text('owner').notNull().references(() => user.id),
|
||||
members: text('members', { mode: "json"}).default([]).notNull(),
|
||||
channels: text('channels', { mode: "json"}).default([]).notNull(), // string[] of ChannelIDs
|
||||
})
|
||||
owner: text('owner')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
members: text('members', { mode: 'json' }).default([]).notNull(),
|
||||
channels: text('channels', { mode: 'json' }).default([]).notNull() // string[] of ChannelIDs
|
||||
});
|
||||
|
||||
export const group = sqliteTable("group", {
|
||||
export const group = sqliteTable('group', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
owner: text('owner').notNull().references(() => user.id),
|
||||
members: text('members', { mode: "json"}).default([]).notNull(),
|
||||
})
|
||||
owner: text('owner')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
members: text('members', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const channel = sqliteTable("channel", {
|
||||
export const channel = sqliteTable('channel', {
|
||||
id: text('id').primaryKey(),
|
||||
name: text('name').notNull(),
|
||||
serverId: text('server_id').notNull().references(() => server.id),
|
||||
messages: text('messages', { mode: "json"}).default([]).notNull(),
|
||||
})
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
messages: text('messages', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const friendRequest = sqliteTable("friendRequest", {
|
||||
export const directMessage = sqliteTable('directMessage', {
|
||||
id: text('id').primaryKey(),
|
||||
fromUser: text('from_user').notNull().references(() => user.id),
|
||||
toUser: text('to_user').notNull().references(() => user.id),
|
||||
})
|
||||
name: text('name').notNull(),
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
messages: text('messages', { mode: 'json' }).default([]).notNull()
|
||||
});
|
||||
|
||||
export const invite = sqliteTable("invite", {
|
||||
export const friendRequest = sqliteTable('friendRequest', {
|
||||
id: text('id').primaryKey(),
|
||||
serverId: text('server_id').notNull().references(() => server.id),
|
||||
code: text('code').notNull(),
|
||||
})
|
||||
fromUser: text('from_user')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
fromUsername: text('from_username')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
toUsername: text('to_username')
|
||||
.notNull()
|
||||
.references(() => user.id),
|
||||
toUser: text('to_user')
|
||||
.notNull()
|
||||
.references(() => user.id)
|
||||
});
|
||||
|
||||
export const invite = sqliteTable('invite', {
|
||||
id: text('id').primaryKey(),
|
||||
serverId: text('server_id')
|
||||
.notNull()
|
||||
.references(() => server.id),
|
||||
code: text('code').notNull()
|
||||
});
|
||||
export type Session = typeof session.$inferSelect;
|
||||
export type User = typeof user.$inferSelect;
|
||||
export type Group = typeof group.$inferSelect;
|
||||
|
|
|
|||
|
|
@ -1,13 +1,13 @@
|
|||
import { clsx, type ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { clsx, type ClassValue } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, "child"> : T;
|
||||
export type WithoutChild<T> = T extends { child?: any } ? Omit<T, 'child'> : T;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, "children"> : T;
|
||||
export type WithoutChildren<T> = T extends { children?: any } ? Omit<T, 'children'> : T;
|
||||
export type WithoutChildrenOrChild<T> = WithoutChildren<WithoutChild<T>>;
|
||||
export type WithElementRef<T, U extends HTMLElement = HTMLElement> = T & { ref?: U | null };
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import favicon from '$lib/assets/favicon.svg';
|
||||
import { ModeWatcher } from "mode-watcher";
|
||||
import { ModeWatcher } from 'mode-watcher';
|
||||
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { fail, redirect } from '@sveltejs/kit';
|
|||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (event.request.url.endsWith("/?logout")) {
|
||||
if (event.request.url.endsWith('/?logout')) {
|
||||
if (!event.locals.session) {
|
||||
return fail(401);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,43 +1,53 @@
|
|||
<script lang="ts">
|
||||
import Title from "$lib/components/extra/Title.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import type { PageServerData } from './$types';
|
||||
import Title from '$lib/components/extra/Title.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import type { PageServerData } from './$types';
|
||||
|
||||
let { data }: { data: PageServerData } = $props();
|
||||
let { data }: { data: PageServerData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="p-6 md:w-1/2">
|
||||
<Title class="text-5xl"></Title>
|
||||
<div class="pb-2">A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on almost every single screen size (responsive design!)</div>
|
||||
<Title class="text-5xl"></Title>
|
||||
<div class="pb-2">
|
||||
A extremely simple chatting application. Supports mobile devices (as a PWA), and also works on
|
||||
almost every single screen size (responsive design!)
|
||||
</div>
|
||||
|
||||
<div class="pb-2">
|
||||
Featurelist:
|
||||
<ul class="list-disc pl-6">
|
||||
<li>Lorem Ipsum is simply dummy text of the printing and typesetting industry. </li>
|
||||
<li>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,</li>
|
||||
<li>when an unknown printer took a galley of type and scrambled it to make a type specimen book.</li>
|
||||
<li>It has survived not only five centuries, but also the leap into electronic typesetting,</li>
|
||||
<li>remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages,</li>
|
||||
</ul>
|
||||
<div class="pb-2">
|
||||
Featurelist:
|
||||
<ul class="list-disc pl-6">
|
||||
<li>Lorem Ipsum is simply dummy text of the printing and typesetting industry.</li>
|
||||
<li>Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,</li>
|
||||
<li>
|
||||
when an unknown printer took a galley of type and scrambled it to make a type specimen book.
|
||||
</li>
|
||||
<li>
|
||||
It has survived not only five centuries, but also the leap into electronic typesetting,
|
||||
</li>
|
||||
<li>
|
||||
remaining essentially unchanged. It was popularised in the 1960s with the release of
|
||||
Letraset sheets containing Lorem Ipsum passages,
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="pb-2">
|
||||
Screenshots
|
||||
|
||||
<div class="grid w-76 grid-cols-2 grid-rows-2 gap-2">
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pb-2">
|
||||
Screenshots
|
||||
|
||||
<div class="grid grid-cols-2 grid-rows-2 w-76 gap-2">
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
|
||||
<img src="https://placehold.co/150x150" alt="temporary placeholder image"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 w-[calc(80%)]">
|
||||
{#if data.user?.id}
|
||||
<Button href='/app'>Enter</Button>
|
||||
<Button href='/?logout'>Log out</Button>
|
||||
{:else}
|
||||
<Button href='/login'>Log in</Button> <Button href='/register'>Register</Button>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex w-[calc(80%)] flex-col gap-3">
|
||||
{#if data.user?.id}
|
||||
<Button href="/app">Enter</Button>
|
||||
<Button href="/?logout">Log out</Button>
|
||||
{:else}
|
||||
<Button href="/login">Log in</Button> <Button href="/register">Register</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
3
src/routes/api/health/+server.ts
Normal file
3
src/routes/api/health/+server.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export async function GET() {
|
||||
return new Response('OK!');
|
||||
}
|
||||
|
|
@ -2,25 +2,23 @@ import { json } from '@sveltejs/kit';
|
|||
import type { RequestHandler } from './$types';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const { groupOrServerId, channelId } = params;
|
||||
const { groupOrServerId, channelId } = params;
|
||||
|
||||
const isGroup = !channelId;
|
||||
const isGroup = !channelId;
|
||||
|
||||
// fake messages
|
||||
const messages = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: crypto.randomUUID(),
|
||||
authorId: `user_${Math.floor(Math.random() * 10)}`,
|
||||
content: isGroup
|
||||
? `Group message #${i + 1}`
|
||||
: `Server message #${i + 1}`,
|
||||
timestamp: Date.now() - Math.floor(Math.random() * 100000),
|
||||
}));
|
||||
// fake messages
|
||||
const messages = Array.from({ length: 5 }, (_, i) => ({
|
||||
id: crypto.randomUUID(),
|
||||
authorId: `user_${Math.floor(Math.random() * 10)}`,
|
||||
content: isGroup ? `Group message #${i + 1}` : `Server message #${i + 1}`,
|
||||
timestamp: Date.now() - Math.floor(Math.random() * 100000)
|
||||
}));
|
||||
|
||||
return json({
|
||||
type: isGroup ? 'group' : 'server',
|
||||
groupId: isGroup ? groupOrServerId : null,
|
||||
serverId: isGroup ? null : groupOrServerId,
|
||||
channelId: channelId ?? null,
|
||||
messages,
|
||||
});
|
||||
return json({
|
||||
type: isGroup ? 'group' : 'server',
|
||||
groupId: isGroup ? groupOrServerId : null,
|
||||
serverId: isGroup ? null : groupOrServerId,
|
||||
channelId: channelId ?? null,
|
||||
messages
|
||||
});
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { Status } from '$lib';
|
||||
|
||||
import { kvStore } from '$lib/server/db';
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const { userId } = params;
|
||||
const { userId } = params;
|
||||
|
||||
const status = Object.values(Status)[Math.floor(Math.random() * Object.values(Status).length)];
|
||||
|
||||
return json({
|
||||
userId,
|
||||
status,
|
||||
lastActive: Date.now() - Math.floor(Math.random() * 600000),
|
||||
customStatus: Math.random() > 0.5 ? 'vibing 🟢' : null,
|
||||
});
|
||||
return json({
|
||||
userId,
|
||||
status: kvStore.get('user-' + userId + '-state'),
|
||||
//@TODO Implement statusmessage
|
||||
statusMessage: Math.random() > 0.5 ? 'vibing 🟢' : null
|
||||
});
|
||||
};
|
||||
|
|
|
|||
48
src/routes/api/updates/+server.ts
Normal file
48
src/routes/api/updates/+server.ts
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
interface SubscribedTo {
|
||||
subscribed: string[];
|
||||
userId: string;
|
||||
controller: ReadableStreamDefaultController;
|
||||
}
|
||||
|
||||
export const _clients = new Map<string, SubscribedTo>();
|
||||
export function _sendToSubscribers(userId: string, payload: unknown) {
|
||||
for (const client of _clients) {
|
||||
if (client[1].subscribed.includes(userId)) {
|
||||
try {
|
||||
client[1].controller.enqueue(`data: ${JSON.stringify(payload)}\n\n`);
|
||||
} catch {
|
||||
_clients.delete(client[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function GET({ locals, request }) {
|
||||
if (!locals.user) {
|
||||
return new Response('No authentication', { status: 401 });
|
||||
}
|
||||
|
||||
const subscribed = locals.user.friends.map((z) => z.id);
|
||||
const reqId = crypto.randomUUID();
|
||||
|
||||
const stream = new ReadableStream({
|
||||
start(controller) {
|
||||
_clients.set(reqId, { subscribed, userId: locals.user!.id, controller });
|
||||
console.log(`SSE Client opened. ${_clients.size}`);
|
||||
|
||||
controller.enqueue(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
||||
|
||||
request.signal.addEventListener('abort', () => {
|
||||
_clients.delete(reqId);
|
||||
console.log(`SSE Client aborted. ${_clients.size}`);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
'Content-Type': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache',
|
||||
Connection: 'keep-alive'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -6,18 +6,17 @@ import * as table from '$lib/server/db/schema';
|
|||
import { FriendRequestID, ServerID } from '$lib';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { and } from 'drizzle-orm';
|
||||
import { User } from '$lib/server/db/schema';
|
||||
import { type User } from '$lib/server/db/schema';
|
||||
export const load: PageServerLoad = async () => {
|
||||
const user = requireLogin();
|
||||
return { user };
|
||||
const user = requireLogin();
|
||||
return { user };
|
||||
};
|
||||
|
||||
|
||||
export const actions = {
|
||||
addFriend: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username');
|
||||
const userId = data.get('userId');
|
||||
addFriend: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const username = data.get('username');
|
||||
const userId = data.get('userId');
|
||||
|
||||
if (username && !userId) {
|
||||
if (typeof username !== 'string' || username.length < 3) {
|
||||
|
|
@ -26,162 +25,193 @@ export const actions = {
|
|||
}
|
||||
|
||||
let user: User[];
|
||||
if(username) {
|
||||
user = await db.select().from(table.user).where(eq(table.user.username, username.toString())).limit(1);
|
||||
} else if(userId) {
|
||||
user = await db.select().from(table.user).where(eq(table.user.id, userId.toString())).limit(1);
|
||||
} else {
|
||||
return fail(400, { error: 'Missing username or userId' });
|
||||
if (username) {
|
||||
user = await db
|
||||
.select()
|
||||
.from(table.user)
|
||||
.where(eq(table.user.username, username.toString()))
|
||||
.limit(1);
|
||||
} else if (userId) {
|
||||
user = await db
|
||||
.select()
|
||||
.from(table.user)
|
||||
.where(eq(table.user.id, userId.toString()))
|
||||
.limit(1);
|
||||
} else {
|
||||
return fail(400, { error: 'Missing username or userId' });
|
||||
}
|
||||
|
||||
if (locals.user?.friends.find((z) => z.id == user[0].id)) {
|
||||
return fail(400, { error: 'Already friends' });
|
||||
}
|
||||
|
||||
if(user?.length == 0)
|
||||
return fail(400, { error: 'User not found' });
|
||||
if (user?.length == 0) return fail(400, { error: 'User not found' });
|
||||
|
||||
const friendRequest = await db.select()
|
||||
.from(table.friendRequest)
|
||||
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
|
||||
const friendRequest = await db
|
||||
.select()
|
||||
.from(table.friendRequest)
|
||||
.where(
|
||||
and(
|
||||
eq(table.friendRequest.fromUser, user[0].id),
|
||||
eq(table.friendRequest.toUser, locals.user!.id)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
// user has already sent a request to us
|
||||
// means we want to accept it
|
||||
//
|
||||
if(friendRequest?.length != 0) {
|
||||
await db.delete(table.friendRequest)
|
||||
.where(and(eq(table.friendRequest.fromUser, user[0].id), eq(table.friendRequest.toUser, locals.user!.id)))
|
||||
// user has already sent a request to us
|
||||
// means we want to accept it
|
||||
//
|
||||
if (friendRequest?.length != 0) {
|
||||
await db
|
||||
.delete(table.friendRequest)
|
||||
.where(
|
||||
and(
|
||||
eq(table.friendRequest.fromUser, user[0].id),
|
||||
eq(table.friendRequest.toUser, locals.user!.id)
|
||||
)
|
||||
)
|
||||
.limit(1);
|
||||
|
||||
// add other guy to us
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.update(table.user)
|
||||
.set({ friends: locals.user?.friends.map(z => z.id).concat(user[0].id) })
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
// add other guy to us
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(table.user)
|
||||
.set({ friends: locals.user?.friends.map((z) => z.id).concat(user[0].id) })
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
|
||||
await tx.update(table.user)
|
||||
.set({ friends: (user[0].friends as string[]).concat(locals.user!.id) })
|
||||
.where(eq(table.user.id, user[0].id));
|
||||
});
|
||||
await tx
|
||||
.update(table.user)
|
||||
.set({ friends: (user[0].friends as string[]).concat(locals.user!.id) })
|
||||
.where(eq(table.user.id, user[0].id));
|
||||
});
|
||||
|
||||
return {success: true}
|
||||
}
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// a request from us has already been sent to user
|
||||
if(locals.user?.friendRequests.find(z => z.toUser == user[0].id && z.fromUser == locals.user!.id))
|
||||
return fail(400, { error: 'Already sent request' });
|
||||
// a request from us has already been sent to user
|
||||
if (
|
||||
locals.user?.friendRequests.find(
|
||||
(z) => z.toUser == user[0].id && z.fromUser == locals.user!.id
|
||||
)
|
||||
)
|
||||
return fail(400, { error: 'Already sent request' });
|
||||
|
||||
await db.insert(table.friendRequest).values({
|
||||
id: FriendRequestID.newV4(),
|
||||
fromUser: locals.user!.id,
|
||||
toUser: user[0].id,
|
||||
});
|
||||
await db.insert(table.friendRequest).values({
|
||||
id: FriendRequestID.newV4(),
|
||||
fromUser: locals.user!.id,
|
||||
toUser: user[0].id
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
cancelFriendRequest: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const requestId = data.get('requestId');
|
||||
return { success: true };
|
||||
},
|
||||
cancelFriendRequest: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const requestId = data.get('requestId');
|
||||
|
||||
if (typeof requestId !== 'string') {
|
||||
return fail(400, { error: 'Invalid request ID' });
|
||||
}
|
||||
if (typeof requestId !== 'string') {
|
||||
return fail(400, { error: 'Invalid request ID' });
|
||||
}
|
||||
|
||||
// fetch the friend request
|
||||
const friendRequest = await db.select()
|
||||
.from(table.friendRequest)
|
||||
.where(eq(table.friendRequest.id, requestId))
|
||||
.limit(1);
|
||||
// fetch the friend request
|
||||
const friendRequest = await db
|
||||
.select()
|
||||
.from(table.friendRequest)
|
||||
.where(eq(table.friendRequest.id, requestId))
|
||||
.limit(1);
|
||||
|
||||
if (!friendRequest?.length) {
|
||||
return fail(404, { error: 'Friend request not found' });
|
||||
}
|
||||
if (!friendRequest?.length) {
|
||||
return fail(404, { error: 'Friend request not found' });
|
||||
}
|
||||
|
||||
const fr = friendRequest[0];
|
||||
const fr = friendRequest[0];
|
||||
|
||||
// only allow cancelling if it's related to current user
|
||||
if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) {
|
||||
return fail(403, { error: 'Not allowed' });
|
||||
}
|
||||
// only allow cancelling if it's related to current user
|
||||
if (fr.fromUser !== locals.user!.id && fr.toUser !== locals.user!.id) {
|
||||
return fail(403, { error: 'Not allowed' });
|
||||
}
|
||||
|
||||
// delete the request
|
||||
await db.delete(table.friendRequest)
|
||||
.where(eq(table.friendRequest.id, requestId))
|
||||
.limit(1);
|
||||
// delete the request
|
||||
await db.delete(table.friendRequest).where(eq(table.friendRequest.id, requestId)).limit(1);
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
createGroup: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const members = data.getAll('member');
|
||||
return { success: true };
|
||||
},
|
||||
createGroup: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const members = data.getAll('member');
|
||||
|
||||
if (!members.length) {
|
||||
return fail(400, { error: 'No members selected' });
|
||||
}
|
||||
if (!members.length) {
|
||||
return fail(400, { error: 'No members selected' });
|
||||
}
|
||||
|
||||
console.log(data, members, locals)
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
joinServer: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const invite = data.get('invite');
|
||||
|
||||
joinServer: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const invite = data.get('invite');
|
||||
if (typeof invite !== 'string') {
|
||||
return fail(400, { error: 'Invalid invite' });
|
||||
}
|
||||
|
||||
if (typeof invite !== 'string') {
|
||||
return fail(400, { error: 'Invalid invite' });
|
||||
}
|
||||
const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1);
|
||||
|
||||
const inv = await db.select().from(table.invite).where(eq(table.invite.code, invite)).limit(1);
|
||||
if (inv?.length == 0) return fail(400, { error: 'Invalid invite' });
|
||||
|
||||
if(inv?.length == 0)
|
||||
return fail(400, { error: 'Invalid invite' });
|
||||
const server = await db
|
||||
.select()
|
||||
.from(table.server)
|
||||
.where(eq(table.server.id, inv[0].serverId))
|
||||
.limit(1);
|
||||
|
||||
const server = await db.select().from(table.server).where(eq(table.server.id, inv[0].serverId)).limit(1);
|
||||
if (server?.length == 0) return fail(400, { error: 'Invalid server' });
|
||||
|
||||
if(server?.length == 0)
|
||||
return fail(400, { error: 'Invalid server' });
|
||||
|
||||
if(locals.user!.servers.some(z => z.id == server[0].id))
|
||||
return fail(400, { error: 'Already in server' });
|
||||
if (locals.user!.servers.some((z) => z.id == server[0].id))
|
||||
return fail(400, { error: 'Already in server' });
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx.update(table.user)
|
||||
.set({servers: locals.user!.servers.map(z => z.id).concat([server[0].id])})
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
await tx
|
||||
.update(table.user)
|
||||
.set({ servers: locals.user!.servers.map((z) => z.id).concat([server[0].id]) })
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
|
||||
await tx.update(table.server)
|
||||
.set({members: (server[0].members as string[]).concat([locals.user!.id])})
|
||||
.where(eq(table.server.id, server[0].id));
|
||||
})
|
||||
await tx
|
||||
.update(table.server)
|
||||
.set({ members: (server[0].members as string[]).concat([locals.user!.id]) })
|
||||
.where(eq(table.server.id, server[0].id));
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
return { success: true };
|
||||
},
|
||||
|
||||
createServer: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const name = data.get('name');
|
||||
createServer: async ({ request, locals }) => {
|
||||
const data = await request.formData();
|
||||
const name = data.get('name');
|
||||
|
||||
if (typeof name !== 'string' || name.length < 3) {
|
||||
return fail(400, { error: 'Server name too short' });
|
||||
}
|
||||
if (typeof name !== 'string' || name.length < 3) {
|
||||
return fail(400, { error: 'Server name too short' });
|
||||
}
|
||||
const serverId = ServerID.newV4();
|
||||
|
||||
await db.insert(table.server)
|
||||
.values({id: serverId, name, owner: locals.user!.id, members: [ locals.user!.id ]});
|
||||
await db
|
||||
.insert(table.server)
|
||||
.values({ id: serverId, name, owner: locals.user!.id, members: [locals.user!.id] });
|
||||
|
||||
await db.update(table.user)
|
||||
.set({servers: locals.user!.servers.map(z => z.id).concat([serverId])})
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
await db
|
||||
.update(table.user)
|
||||
.set({ servers: locals.user!.servers.map((z) => z.id).concat([serverId]) })
|
||||
.where(eq(table.user.id, locals.user!.id));
|
||||
|
||||
redirect(303, `/app`);
|
||||
}
|
||||
redirect(303, `/app`);
|
||||
}
|
||||
} satisfies Actions;
|
||||
|
||||
function requireLogin() {
|
||||
const { locals } = getRequestEvent();
|
||||
const { locals } = getRequestEvent();
|
||||
|
||||
if (!locals.user) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
if (!locals.user) {
|
||||
return redirect(302, '/login');
|
||||
}
|
||||
|
||||
return locals.user;
|
||||
return locals.user;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,27 @@
|
|||
<script lang="ts">
|
||||
import { Status, type OverviewData,
|
||||
GroupID, UserID, ServerID,
|
||||
type GroupId, type ServerId, type UserId,
|
||||
type OverviewUser, type OverviewGroup, type OverviewServer,
|
||||
type UserWithStatus} from "$lib";
|
||||
import {
|
||||
Status,
|
||||
type OverviewData,
|
||||
GroupID,
|
||||
UserID,
|
||||
ServerID,
|
||||
type GroupId,
|
||||
type ServerId,
|
||||
type UserId,
|
||||
type OverviewUser,
|
||||
type OverviewGroup,
|
||||
type OverviewServer,
|
||||
type UserWithStatus
|
||||
} from '$lib';
|
||||
import type { PageServerData } from './$types';
|
||||
import AppSidebar from "$lib/components/app-sidebar.svelte";
|
||||
import * as Sidebar from "$lib/components/ui/sidebar/index.js";
|
||||
import * as AlertDialog from "$lib/components/ui/alert-dialog/index.js";
|
||||
import { onMount } from "svelte";
|
||||
import type { ActionData } from './$types';
|
||||
import AppSidebar from '$lib/components/app-sidebar.svelte';
|
||||
import * as Sidebar from '$lib/components/ui/sidebar/index.js';
|
||||
import * as AlertDialog from '$lib/components/ui/alert-dialog/index.js';
|
||||
import { onMount } from 'svelte';
|
||||
import type { ActionData } from './$types';
|
||||
let errorOpen = $state(true);
|
||||
let { form, data }: { form: ActionData, data: PageServerData } = $props();
|
||||
let currentPageID: (UserId|GroupId|ServerId)|null = $state(null);
|
||||
let { form, data }: { form: ActionData; data: PageServerData } = $props();
|
||||
let currentPageID: (UserId | GroupId | ServerId) | null = $state(null);
|
||||
let currentPage: OverviewUser | OverviewGroup | OverviewServer | undefined = $state();
|
||||
|
||||
const overview_data: OverviewData = $state({
|
||||
|
|
@ -21,84 +30,101 @@
|
|||
servers: []
|
||||
});
|
||||
|
||||
console.log(form, data, overview_data)
|
||||
|
||||
$effect(() => {
|
||||
if (currentPageID) {
|
||||
if(UserID.is(currentPageID)) {
|
||||
currentPage = overview_data.friends.find(friend => friend.id === currentPageID);
|
||||
} else if(GroupID.is(currentPageID)) {
|
||||
currentPage = overview_data.groups.find(group => group.id === currentPageID);
|
||||
} else if(ServerID.is(currentPageID)) {
|
||||
currentPage = overview_data.servers.find(server => server.id === currentPageID);
|
||||
if (UserID.is(currentPageID)) {
|
||||
currentPage = overview_data.friends.find((friend) => friend.id === currentPageID);
|
||||
} else if (GroupID.is(currentPageID)) {
|
||||
currentPage = overview_data.groups.find((group) => group.id === currentPageID);
|
||||
} else if (ServerID.is(currentPageID)) {
|
||||
currentPage = overview_data.servers.find((server) => server.id === currentPageID);
|
||||
}
|
||||
} else {
|
||||
currentPage = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
onMount(()=>{
|
||||
onMount(() => {
|
||||
async function run() {
|
||||
overview_data.servers = data.user.servers.map(z => {
|
||||
overview_data.servers = data.user.servers.map((z) => {
|
||||
return {
|
||||
id: ServerID.parse(z.id),
|
||||
name: z.name,
|
||||
ownerId: z.ownerId,
|
||||
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + z.name,
|
||||
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
|
||||
};
|
||||
})
|
||||
overview_data.groups = data.user.groups.map(z => {
|
||||
});
|
||||
overview_data.groups = data.user.groups.map((z) => {
|
||||
return {
|
||||
id: GroupID.parse(z.id),
|
||||
name: z.name,
|
||||
ownerId: z.ownerId,
|
||||
members: z.members,
|
||||
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + z.name,
|
||||
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + z.name
|
||||
};
|
||||
})
|
||||
});
|
||||
overview_data.friends = await Promise.all(
|
||||
data.user.friends.map(async (friend) => {
|
||||
const res = await fetch(`/api/status/${friend.id}`);
|
||||
data.user.friends.map(async (friend) => {
|
||||
const res = await fetch(`/api/status/${friend.id}`);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch status for ${friend.id}`);
|
||||
}
|
||||
if (!res.ok) {
|
||||
throw new Error(`Failed to fetch status for ${friend.id}`);
|
||||
}
|
||||
|
||||
const status = await res.json();
|
||||
const status = await res.json();
|
||||
|
||||
return {
|
||||
id: UserID.parse(friend.id),
|
||||
username: friend.username,
|
||||
status: status.status,
|
||||
customStatus: status.customStatus,
|
||||
image: "https://api.dicebear.com/7.x/pixel-art/svg?seed=" + friend.username
|
||||
};
|
||||
})
|
||||
return {
|
||||
id: UserID.parse(friend.id),
|
||||
username: friend.username,
|
||||
status: status.status,
|
||||
statusMessage: status.statusMessage,
|
||||
image: 'https://api.dicebear.com/7.x/pixel-art/svg?seed=' + friend.username
|
||||
};
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
run();
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const sse = new EventSource('/api/updates');
|
||||
|
||||
sse.addEventListener('message', (e) => {
|
||||
const json = JSON.parse(e.data) as
|
||||
| { type: 'connected' }
|
||||
| { type: 'status'; id: string; status: 1 | 2 | 3 };
|
||||
|
||||
if (json.type == 'connected') {
|
||||
console.log('SSE connected.');
|
||||
}
|
||||
|
||||
if (json.type == 'status') {
|
||||
const friend = overview_data.friends.find((z) => z.id == json.id);
|
||||
if (friend) {
|
||||
friend.status = json.status;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if form}
|
||||
<AlertDialog.Root bind:open={errorOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>{form?.error ? "Ran into an error." : "Success!"}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{form?.error || "Action completed succesfully."}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
<AlertDialog.Root bind:open={errorOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>{form?.error ? 'Ran into an error.' : 'Success!'}</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
{form?.error || 'Action completed succesfully.'}
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel>Close</AlertDialog.Cancel>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
{/if}
|
||||
|
||||
|
||||
<Sidebar.Provider>
|
||||
<AppSidebar bind:currentPage={currentPageID} user={data.user} data={overview_data} />
|
||||
|
||||
|
|
@ -107,24 +133,32 @@
|
|||
<Sidebar.Trigger class="-ms-1" />
|
||||
{#if currentPageID && currentPage}
|
||||
{#if ServerID.is(currentPageID)}
|
||||
{@const server = (currentPage as OverviewServer)}
|
||||
{@const server = currentPage as OverviewServer}
|
||||
|
||||
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
|
||||
<img src={server!.image} alt={server!.name} class="size-6 rounded-full" />
|
||||
|
||||
<h1>{server!.name}</h1>
|
||||
<h1>{server!.name}</h1>
|
||||
{:else if UserID.is(currentPageID)}
|
||||
{@const friend = (currentPage as UserWithStatus)}
|
||||
{@const friend = currentPage as UserWithStatus}
|
||||
|
||||
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
|
||||
<img src={friend.image} alt={friend!.username} class="size-6 rounded-full" />
|
||||
|
||||
<h1>{friend!.username} [{friend.status == Status.ONLINE ? "Online!" : friend.status == Status.DND ? "DND" : friend.status == Status.OFFLINE ? "Offline" : "Unknown"}]</h1>
|
||||
<h1>
|
||||
{friend!.username} [{friend.status == Status.ONLINE
|
||||
? 'Online!'
|
||||
: friend.status == Status.DND
|
||||
? 'DND'
|
||||
: friend.status == Status.OFFLINE
|
||||
? 'Offline'
|
||||
: 'Unknown'}]
|
||||
</h1>
|
||||
{:else if GroupID.is(currentPageID)}
|
||||
{@const group = (currentPage as OverviewGroup)}
|
||||
{@const group = currentPage as OverviewGroup}
|
||||
|
||||
<h1>{group!.name} ({group.members} member{group.members > 1 ? "s" : ""})</h1>
|
||||
<h1>{group!.name} ({group.members} member{group.members > 1 ? 's' : ''})</h1>
|
||||
{/if}
|
||||
{/if}
|
||||
</header>
|
||||
<h1> this is like lowkirkounely the content, i should put messages and shi here</h1>
|
||||
<h1>this is like lowkirkounely the content, i should put messages and shi here</h1>
|
||||
</Sidebar.Inset>
|
||||
</Sidebar.Provider>
|
||||
|
|
|
|||
|
|
@ -7,56 +7,59 @@ import * as table from '$lib/server/db/schema';
|
|||
import type { Actions, PageServerLoad } from './$types';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/demo/lucia');
|
||||
}
|
||||
return {};
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/demo/lucia');
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
login: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
login: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const username = formData.get('username');
|
||||
const password = formData.get('password');
|
||||
|
||||
let username_is_email = false;
|
||||
|
||||
if (!auth.validateUsername(username)) {
|
||||
if (!auth.validateUsername(username)) {
|
||||
if (auth.validateEmail(username)) {
|
||||
username_is_email = true;
|
||||
} else {
|
||||
return fail(400, {
|
||||
message: 'Invalid username (min 3, max 31 characters, alphanumeric only)'
|
||||
});
|
||||
return fail(400, {
|
||||
message: 'Invalid username (min 3, max 31 characters, alphanumeric only)'
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!auth.validatePassword(password)) {
|
||||
return fail(400, { message: 'Invalid password (min 6, max 255 characters)' });
|
||||
}
|
||||
}
|
||||
if (!auth.validatePassword(password)) {
|
||||
return fail(400, { message: 'Invalid password (min 6, max 255 characters)' });
|
||||
}
|
||||
|
||||
const results = await db.select()
|
||||
.from(table.user)
|
||||
.where(username_is_email ? eq(table.user.email, username) : eq(table.user.username, username));
|
||||
const results = await db
|
||||
.select()
|
||||
.from(table.user)
|
||||
.where(
|
||||
username_is_email ? eq(table.user.email, username) : eq(table.user.username, username)
|
||||
);
|
||||
|
||||
const existingUser = results.at(0);
|
||||
if (!existingUser) {
|
||||
return fail(400, { message: 'Incorrect username or password' });
|
||||
}
|
||||
const existingUser = results.at(0);
|
||||
if (!existingUser) {
|
||||
return fail(400, { message: 'Incorrect username or password' });
|
||||
}
|
||||
|
||||
const validPassword = await verify(existingUser.passwordHash, password, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
if (!validPassword) {
|
||||
return fail(400, { message: 'Incorrect username or password' });
|
||||
}
|
||||
const validPassword = await verify(existingUser.passwordHash, password, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
if (!validPassword) {
|
||||
return fail(400, { message: 'Incorrect username or password' });
|
||||
}
|
||||
|
||||
const sessionToken = auth.generateSessionToken();
|
||||
const session = await auth.createSession(sessionToken, existingUser.id);
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
const sessionToken = auth.generateSessionToken();
|
||||
const session = await auth.createSession(sessionToken, existingUser.id);
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
|
||||
return redirect(302, '/app');
|
||||
}
|
||||
return redirect(302, '/app');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,26 +1,24 @@
|
|||
<script lang="ts">
|
||||
import Title from "$lib/components/extra/Title.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import type { ActionData } from './$types';
|
||||
import Title from '$lib/components/extra/Title.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
let { form }: { form: ActionData } = $props();
|
||||
let { form }: { form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen flex-col items-center justify-center gap-2">
|
||||
<div class='w-1/2 p-2 bg-secondary rounded-md '>
|
||||
<div class="text-center pb-2">
|
||||
<Title class="text-2xl"></Title>
|
||||
</div>
|
||||
<form method="post" action="?/login">
|
||||
<Input name="username" class="mb-2" type="text" placeholder="Username or E-mail"></Input>
|
||||
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
|
||||
|
||||
<Button type="submit" formaction="?/login" class="w-full">Login</Button>
|
||||
</form>
|
||||
|
||||
<p style="color: red">{form?.message ?? ''}</p>
|
||||
|
||||
|
||||
<div class="w-1/2 rounded-md bg-secondary p-2">
|
||||
<div class="pb-2 text-center">
|
||||
<Title class="text-2xl"></Title>
|
||||
</div>
|
||||
<form method="post" action="?/login">
|
||||
<Input name="username" class="mb-2" type="text" placeholder="Username or E-mail"></Input>
|
||||
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
|
||||
|
||||
<Button type="submit" formaction="?/login" class="w-full">Login</Button>
|
||||
</form>
|
||||
|
||||
<p style="color: red">{form?.message ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,59 +9,58 @@ import { eq } from 'drizzle-orm';
|
|||
import { UserID } from '$lib';
|
||||
|
||||
export const load: PageServerLoad = async (event) => {
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/demo/lucia');
|
||||
}
|
||||
return {};
|
||||
if (event.locals.user) {
|
||||
return redirect(302, '/demo/lucia');
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
register: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const username = formData.get('username');
|
||||
const email = formData.get('email');
|
||||
const password = formData.get('password');
|
||||
register: async (event) => {
|
||||
const formData = await event.request.formData();
|
||||
const username = formData.get('username');
|
||||
const email = formData.get('email');
|
||||
const password = formData.get('password');
|
||||
|
||||
if (!auth.validateUsername(username)) {
|
||||
return fail(400, { message: 'Invalid username' });
|
||||
}
|
||||
if (!auth.validateUsername(username)) {
|
||||
return fail(400, { message: 'Invalid username' });
|
||||
}
|
||||
|
||||
if(!auth.validateEmail(email)) {
|
||||
return fail(400, { message: 'Invalid email' });
|
||||
}
|
||||
if (!auth.validateEmail(email)) {
|
||||
return fail(400, { message: 'Invalid email' });
|
||||
}
|
||||
|
||||
if (!auth.validatePassword(password)) {
|
||||
return fail(400, { message: 'Invalid password' });
|
||||
}
|
||||
if (!auth.validatePassword(password)) {
|
||||
return fail(400, { message: 'Invalid password' });
|
||||
}
|
||||
|
||||
const userId = UserID.newV4();
|
||||
const passwordHash = await hash(password, {
|
||||
// recommended minimum parameters
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
const results = await db.select()
|
||||
.from(table.user)
|
||||
.where(or(eq(table.user.email, email),
|
||||
eq(table.user.username, username)));
|
||||
const passwordHash = await hash(password, {
|
||||
// recommended minimum parameters
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1
|
||||
});
|
||||
const results = await db
|
||||
.select()
|
||||
.from(table.user)
|
||||
.where(or(eq(table.user.email, email), eq(table.user.username, username)));
|
||||
|
||||
const existingUser = results.at(0);
|
||||
if (existingUser) {
|
||||
return fail(400, { message: 'Username or email already registered!' });
|
||||
}
|
||||
const existingUser = results.at(0);
|
||||
if (existingUser) {
|
||||
return fail(400, { message: 'Username or email already registered!' });
|
||||
}
|
||||
|
||||
try {
|
||||
await db.insert(table.user)
|
||||
.values({ id: userId, email, username, passwordHash });
|
||||
try {
|
||||
await db.insert(table.user).values({ id: userId, email, username, passwordHash });
|
||||
|
||||
const sessionToken = auth.generateSessionToken();
|
||||
const session = await auth.createSession(sessionToken, userId);
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
} catch {
|
||||
return fail(500, { message: 'An error has occurred' });
|
||||
}
|
||||
return redirect(302, '/app');
|
||||
}
|
||||
const sessionToken = auth.generateSessionToken();
|
||||
const session = await auth.createSession(sessionToken, userId);
|
||||
auth.setSessionTokenCookie(event, sessionToken, session.expiresAt);
|
||||
} catch {
|
||||
return fail(500, { message: 'An error has occurred' });
|
||||
}
|
||||
return redirect(302, '/app');
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,25 +1,25 @@
|
|||
<script lang="ts">
|
||||
import Title from "$lib/components/extra/Title.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import type { ActionData } from './$types';
|
||||
import Title from '$lib/components/extra/Title.svelte';
|
||||
import Button from '$lib/components/ui/button/button.svelte';
|
||||
import Input from '$lib/components/ui/input/input.svelte';
|
||||
import type { ActionData } from './$types';
|
||||
|
||||
let { form }: { form: ActionData } = $props();
|
||||
let { form }: { form: ActionData } = $props();
|
||||
</script>
|
||||
|
||||
<div class="flex min-h-screen flex-col items-center justify-center gap-2">
|
||||
<div class='w-1/2 p-2 bg-secondary rounded-md '>
|
||||
<div class="text-center pb-2">
|
||||
<Title class="text-2xl"></Title>
|
||||
</div>
|
||||
<form method="post" action="?/register">
|
||||
<Input name="username" class="mb-2" type="text" placeholder="Username"></Input>
|
||||
<Input name="email" class="mb-2" type="email" placeholder="E-mail"></Input>
|
||||
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
|
||||
|
||||
<Button type="submit" formaction="?/register" class="w-full">Register</Button>
|
||||
</form>
|
||||
|
||||
<p style="color: red">{form?.message ?? ''}</p>
|
||||
<div class="w-1/2 rounded-md bg-secondary p-2">
|
||||
<div class="pb-2 text-center">
|
||||
<Title class="text-2xl"></Title>
|
||||
</div>
|
||||
<form method="post" action="?/register">
|
||||
<Input name="username" class="mb-2" type="text" placeholder="Username"></Input>
|
||||
<Input name="email" class="mb-2" type="email" placeholder="E-mail"></Input>
|
||||
<Input name="password" class="mb-2" type="password" placeholder="Password"></Input>
|
||||
|
||||
<Button type="submit" formaction="?/register" class="w-full">Register</Button>
|
||||
</form>
|
||||
|
||||
<p style="color: red">{form?.message ?? ''}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue