actually start implementing the backend slowly
This commit is contained in:
parent
37ae49b66e
commit
342fd30d62
19 changed files with 977 additions and 136 deletions
|
|
@ -1,5 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { Status, type Friend , type Group, type Server } from "$lib";
|
||||
import { type Data } 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";
|
||||
|
|
@ -11,9 +11,9 @@
|
|||
import CirclePlus from '@lucide/svelte/icons/circle-plus';
|
||||
import Input from "./ui/input/input.svelte";
|
||||
import Button, { buttonVariants } from "./ui/button/button.svelte";
|
||||
import FriendComponent from "./Friend.svelte";
|
||||
import User from "./extra/User.svelte";
|
||||
|
||||
let { currentPage = $bindable<string|null>(), data, ...restProps }: {currentPage: string|null, data: { friends: Friend[], groups: Group[], servers: Server[] }}= $props();
|
||||
let { currentPage = $bindable<string|null>(), data, ...restProps }: {currentPage: string|null, data: Data }= $props();
|
||||
</script>
|
||||
|
||||
<Sidebar.Root {...restProps}>
|
||||
|
|
@ -33,75 +33,126 @@
|
|||
<div class="w-full flex gap-2 justify-center">
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UserRoundPlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<Input></Input>
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Send request.</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<form method="POST" action="?/addFriend">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Add a friend</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add a friend using their username.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<Input name="username" placeholder="username" required />
|
||||
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>
|
||||
Cancel
|
||||
</Dialog.Close>
|
||||
<Button type="submit">Send request</Button>
|
||||
</Dialog.Footer>
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<Dialog.Root>
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create a group</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Add friends into your group!
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
{#each data.friends as friend (friend.id)}
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<UsersRound />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
<FriendComponent onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}} {friend}></FriendComponent>
|
||||
<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>
|
||||
|
||||
{/each}
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</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.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
<Dialog.Trigger>
|
||||
<Button variant="outline" size="icon">
|
||||
<CirclePlus />
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
|
||||
</Button>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Content class="sm:max-w-[425px]">
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Join a server</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Enter it's link into the input box here.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
<Input></Input>
|
||||
<Dialog.Footer>
|
||||
<Dialog.Close class={buttonVariants({ variant: "outline" })}>Cancel</Dialog.Close>
|
||||
<Button type="submit">Create group</Button>
|
||||
</Dialog.Footer>
|
||||
</Dialog.Content>
|
||||
<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>
|
||||
|
||||
</div>
|
||||
</Sidebar.MenuItem>
|
||||
</Sidebar.Menu>
|
||||
|
|
@ -128,10 +179,10 @@
|
|||
<Sidebar.MenuSubItem>
|
||||
<Sidebar.MenuSubButton>
|
||||
|
||||
<FriendComponent onclick={(e) => {
|
||||
<User onclick={(e) => {
|
||||
e.preventDefault();
|
||||
currentPage = friend.id;
|
||||
}} {friend}></FriendComponent>
|
||||
}} user={friend}></User>
|
||||
</Sidebar.MenuSubButton>
|
||||
</Sidebar.MenuSubItem>
|
||||
{/each}
|
||||
|
|
@ -194,7 +245,7 @@
|
|||
e.preventDefault();
|
||||
currentPage = server.id;
|
||||
}} href="##" class="flex items-center gap-2">
|
||||
<img src={server.image} alt={server.name} class="size-6 rounded-full" />
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { Status, type Friend } from '$lib';
|
||||
|
||||
import { Status, type UserWithStatus } from '$lib';
|
||||
|
||||
const {
|
||||
onclick,
|
||||
friend
|
||||
}: { onclick?: (e: MouseEvent) => void, friend: Friend } = $props();
|
||||
user
|
||||
}: { onclick?: (e: MouseEvent) => void, user: UserWithStatus } = $props();
|
||||
</script>
|
||||
|
||||
<a {onclick} href="##" class="flex items-center gap-2">
|
||||
<div class="relative">
|
||||
<img src={friend.image} alt={friend.name} class="size-6 rounded-full" />
|
||||
{#if friend.status === Status.OFFLINE}
|
||||
<img src={"https://api.dicebear.com/7.x/pixel-art/svg?seed=" + user.username} alt={user.username} class="size-6 rounded-full" />
|
||||
{#if user.status === Status.OFFLINE}
|
||||
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-gray-500 ring-1 ring-white"></span>
|
||||
{:else if friend.status === Status.DND}
|
||||
{:else if user.status === Status.DND}
|
||||
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-red-500 ring-1 ring-white"></span>
|
||||
{:else if friend.status === Status.ONLINE}
|
||||
{:else if user.status === Status.ONLINE}
|
||||
<span class="absolute bottom-0 end-0 block size-2 rounded-full bg-green-500 ring-1 ring-white"></span>
|
||||
{/if}
|
||||
</div>
|
||||
{friend.name}
|
||||
{user.username}
|
||||
</a>
|
||||
|
|
@ -8,33 +8,39 @@ export type UserId = Puuid<"user">;
|
|||
export type GroupId = Puuid<"group">;
|
||||
export type ServerId = Puuid<"srv">;
|
||||
|
||||
|
||||
export const Status: Record<string, 1|2|3> = {
|
||||
OFFLINE: 1,
|
||||
DND: 2,
|
||||
ONLINE: 3
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: UserId
|
||||
name: string,
|
||||
status: 1|2|3,
|
||||
image: string
|
||||
}
|
||||
export interface Group {
|
||||
id: GroupId
|
||||
name: string
|
||||
members: number
|
||||
}
|
||||
export interface Server {
|
||||
id: ServerId
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
|
||||
|
||||
export interface Data {
|
||||
friends: User[],
|
||||
groups: Group[],
|
||||
servers: Server[],
|
||||
export type OverviewUser = {
|
||||
id: string;
|
||||
username: string;
|
||||
image: string;
|
||||
};
|
||||
|
||||
export type OverviewServer = {
|
||||
id: string;
|
||||
name: string;
|
||||
ownerId: string;
|
||||
image: string;
|
||||
};
|
||||
export type OverviewGroup = {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import type { RequestEvent } from '@sveltejs/kit';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { eq, inArray } from 'drizzle-orm';
|
||||
import { sha256 } from '@oslojs/crypto/sha2';
|
||||
import { encodeBase32LowerCase, encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { encodeBase64url, encodeHexLowerCase } from '@oslojs/encoding';
|
||||
import { db } from '$lib/server/db';
|
||||
import * as table from '$lib/server/db/schema';
|
||||
|
||||
|
|
@ -28,20 +28,25 @@ export async function createSession(token: string, userId: string) {
|
|||
|
||||
export async function validateSessionToken(token: string) {
|
||||
const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token)));
|
||||
const [result] = await db
|
||||
const [row] = await db
|
||||
.select({
|
||||
// Adjust user table here to tweak returned data
|
||||
user: { id: table.user.id, username: table.user.username },
|
||||
session: table.session
|
||||
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));
|
||||
|
||||
if (!result) {
|
||||
if (!row) {
|
||||
return { session: null, user: null };
|
||||
}
|
||||
const { session, user } = result;
|
||||
const { session, user } = row;
|
||||
|
||||
const sessionExpired = Date.now() >= session.expiresAt.getTime();
|
||||
if (sessionExpired) {
|
||||
|
|
@ -57,8 +62,40 @@ export async function validateSessionToken(token: string) {
|
|||
.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[])))
|
||||
: [];
|
||||
|
||||
return { session, user };
|
||||
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[])))
|
||||
: [];
|
||||
|
||||
|
||||
return { session, user: {...user, servers, friends, groups: groups.map(z => { return { ...z, members: (z.members as string[]).length}})} };
|
||||
}
|
||||
|
||||
export type SessionValidationResult = Awaited<ReturnType<typeof validateSessionToken>>;
|
||||
|
|
@ -80,14 +117,6 @@ export function deleteSessionTokenCookie(event: RequestEvent) {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
export function generateUserId() {
|
||||
// ID with 120 bits of entropy, or about the same as UUID v4.
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(15));
|
||||
const id = encodeBase32LowerCase(bytes);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function validateUsername(username: unknown): username is string {
|
||||
return (
|
||||
typeof username === 'string' &&
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
import { drizzle } from 'drizzle-orm/bun-sqlite';
|
||||
import { Database } from 'bun:sqlite';
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@ export const user = sqliteTable('user', {
|
|||
email: text('email').notNull().unique(),
|
||||
passwordHash: text('password_hash').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
|
||||
});
|
||||
|
||||
export const session = sqliteTable('session', {
|
||||
|
|
@ -21,7 +24,7 @@ export const server = sqliteTable("server", {
|
|||
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(),
|
||||
channels: text('channels', { mode: "json"}).default([]).notNull(), // string[] of ChannelIDs
|
||||
})
|
||||
|
||||
export const group = sqliteTable("group", {
|
||||
|
|
@ -31,6 +34,13 @@ export const group = sqliteTable("group", {
|
|||
members: text('members', { mode: "json"}).default([]).notNull(),
|
||||
})
|
||||
|
||||
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(),
|
||||
})
|
||||
|
||||
export type Session = typeof session.$inferSelect;
|
||||
export type User = typeof user.$inferSelect;
|
||||
export type Group = typeof group.$inferSelect;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue