Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
**/minio-storage
**/vite.config.js.timestamp-*
**/vite.config.ts.timestamp-*
tests
.output
.vercel
.netlify
Expand Down
64 changes: 64 additions & 0 deletions .github/workflows/playwright.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
name: Tests
on: [push, pull_request]

jobs:
test:
name: Tests
runs-on: ubuntu-latest
services:
# Cassandra service container
cassandra:
image: cassandra:latest
ports:
- 9042:9042
options: >-
--health-cmd="nodetool status"
--health-interval=10s
--health-timeout=5s
--health-retries=5
env:
CASSANDRA_USER: admin
CASSANDRA_PASSWORD: admin
# Minio service container
minio:
image: docker.io/bitnami/minio
ports:
- 9000:9000
env:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- name: Install dependencies
run: bun install

- name: Install Playwright browsers
run: bunx playwright install --with-deps chromium

- name: Set up users DB
run: bun run migrate

- name: Setup environment
run: cp .env.example .env

- name: Run Playwright tests
run: |
bun run dev --host &
bun run test
env:
NODE_ENV: testing

- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: test-results
path: test-results/
retention-days: 30
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ Thumbs.db
!.env.example
!.env.test

# Tests
test-results

# Vite
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ A simple chat app built with SvelteKit and Apache Cassandra<br>

[![Prettier](https://github.com/arithefirst/sv-chat/actions/workflows/prettier.yml/badge.svg)](https://github.com/arithefirst/sv-chat/actions/workflows/prettier.yml)
[![ESLint](https://github.com/arithefirst/sv-chat/actions/workflows/eslint.yml/badge.svg)](https://github.com/arithefirst/sv-chat/actions/workflows/eslint.yml)
[![Playwright](https://github.com/arithefirst/sv-chat/actions/workflows/playwright.yml/badge.svg)](https://github.com/arithefirst/sv-chat/actions/workflows/playwright.yml)

## 💻 Techstack

Expand Down
Binary file modified bun.lockb
Binary file not shown.
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
"minio": "minio server minio-storage",
"preview": "vite preview",
"prepare": "svelte-kit sync || echo ''",
"production": "tsm ./prodServer.ts"
"production": "tsm ./prodServer.ts",
"test": "playwright test",
"test:head": "playwright test --headed"
},
"devDependencies": {
"@eslint/compat": "^1.2.5",
Expand All @@ -37,6 +39,7 @@
"typescript-eslint": "^8.20.0"
},
"dependencies": {
"@playwright/test": "^1.50.1",
"@sveltejs/adapter-node": "^5.2.12",
"@tailwindcss/typography": "^0.5.16",
"@types/better-sqlite3": "^7.6.12",
Expand Down
29 changes: 29 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: 'tests',
projects: [
{
name: 'setup',
testMatch: /setup\.ts/,
use: { ...devices['Desktop Chrome'] },
},

// Make all other tests depend on the signup,
// since they need user accounts to run

{
name: 'test',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
testMatch: /(.+\.)?(test|spec)\.[jt]s/,
},
],
retries: process.env.CI ? 1 : 0,
reporter: 'list',
workers: 1,
use: {
baseURL: 'http://localhost:5173',
trace: 'on-first-retry',
},
});
6 changes: 3 additions & 3 deletions src/lib/components/mainLayout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
</script>

<div class="w-screen">
<div class="grid min-h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<div class="grid h-screen w-full md:grid-cols-[220px_1fr] lg:grid-cols-[280px_1fr]">
<div class="hidden border-r bg-muted/40 md:block" bind:clientWidth={sidebarWidth}>
<div class="flex h-full max-h-screen flex-col gap-2">
<div class="flex h-14 items-center border-b px-4 lg:h-[60px] lg:px-6">
Expand All @@ -28,14 +28,14 @@
</a>
<ModeSwitcher />
</div>
<div class="flex-1">
<div class="h-full flex-1 overflow-scroll">
<nav class="grid items-start px-2 text-sm font-medium lg:px-4">
{#each channels as channelName}
<Channel {channelName} />
{/each}
</nav>
</div>
<div class="mt-auto p-4">
<div class="mt-auto border-t p-4">
<User {data} />
<ChannelDialog data={data.form} />
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/components/user.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
<div class="mb-1 flex w-full items-center p-2">
<div class="avatar mr-2 rounded-sm">
<div class="h-12 w-12 overflow-hidden rounded-lg border bg-white">
<img src={imageSrc} alt="Profile image for {data.user.username}" />
<img src={imageSrc} alt="Profile image for {data.user.username}" id="userimage" />
</div>
</div>
<div class="flex w-full items-center align-middle">
<p class="font-bold">{data.user.username}</p>
<p class="font-bold" id="currentuser-username">{data.user.username}</p>
</div>
<Tooltip.Root>
<Tooltip.Trigger>
Expand Down
8 changes: 4 additions & 4 deletions src/lib/server/db/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import cassandra from 'cassandra-driver';
import 'dotenv/config';

interface Messages {
type Messages = {
messages: cassandra.types.Row[] | null;
error: Error | null;
}
};

function createDelay(ms: number) {
return new Promise((res) => setTimeout(res, ms));
Expand Down Expand Up @@ -36,8 +36,8 @@ class Db {
try {
await this.client.connect();
break;
} catch {
console.error(`Error communicating with DB. Retrying...`);
} catch (e) {
console.error(`Error communicating with DB (${this.clientUrl}:9042). Retrying.. ${(e as Error).message}`);
await createDelay(1000);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/lib/server/db/sqlite.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Database from 'better-sqlite3';

interface Profile {
type Profile = {
username: string;
image: string;
}
};

class AuthDb {
private client = new Database('./src/lib/server/db/users.db');
Expand Down
14 changes: 10 additions & 4 deletions src/lib/server/storage/minio-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import * as Minio from 'minio';
import { Readable } from 'stream';
import { v4 } from 'uuid';

interface ClientParams {
type ClientParams = {
endPoint: string;
port: number;
accessKey: string;
secretKey: string;
useSSL: boolean;
}
};

class MinioClient {
private client: Minio.Client;
Expand Down Expand Up @@ -51,7 +51,11 @@ class MinioClient {
const bucket = 'profile-photos';
if (!(await this.client.bucketExists(bucket))) {
console.log(`\x1b[35m[S3]\x1b[0m Creating bucket '${bucket}', as it is required but does not exist.`);
this.client.makeBucket(bucket);
try {
await this.client.makeBucket(bucket);
} catch (e) {
console.error((e as Error).message);
}
}

const objectId = `${v4()}${this.getFileExtension(mime)}`;
Expand All @@ -62,7 +66,9 @@ class MinioClient {
etag: upload.etag,
};
} catch (e) {
console.error(`Error uploading file: ${(e as Error).message}`);
if ((e as Error).message !== 'Unsupported file type') {
console.error(`Error uploading file: ${(e as Error).message}`);
}
throw e;
}
}
Expand Down
10 changes: 5 additions & 5 deletions src/lib/types/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ export const changePasswordSchema = z
newPassword: z
.string()
.min(8, 'New password must be at least 8 characters.')
.regex(/(?=.*[A-Z])/gm, 'New password must contain at uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'New password must contain at lowercase letter.')
.regex(/(?=.*[A-Z])/gm, 'New password must contain an uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'New password must contain a lowercase letter.')
.regex(/(?=.*\d)/gm, 'New password must contain at least one number.')
.regex(/(?=.*\W)/gm, 'New password must contain at least one special character'),
.regex(/(?=.*\W)/gm, 'New password must contain at least one special character.'),
})
.refine((schema) => schema.newPassword !== 'Password123!', {
message: "You can't use the example password, silly",
Expand All @@ -25,8 +25,8 @@ export const changeUsernameSchema = z.object({
.string()
.min(3, 'Username must be at least 3 characters.')
.max(15, 'Username must be no more than 15 characters.')
.regex(/^(?![A-Z])/gm, 'Username cannot contain uppercase letters')
.regex(/^(?=[a-z0-9-_]+$)/gm, 'Username cannot contain special characters'),
.regex(/^(?![A-Z])/gm, 'Username cannot contain uppercase letters.')
.regex(/^(?=[a-z0-9-_]+$)/gm, 'Username cannot contain special characters.'),
});

export type ChangePasswordSchema = typeof changePasswordSchema;
Expand Down
13 changes: 4 additions & 9 deletions src/lib/types/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
export interface TypeMessage {
export type TypeMessage = {
message: string;
imageSrc: string;
user: string;
uid: string;
timestamp: Date;
}
};

export interface TypeFullMessage {
export type TypeFullMessage = TypeMessage & {
channel: string;
message: string;
imageSrc: string;
user: string;
uid: string;
timestamp: Date;
}
};
6 changes: 5 additions & 1 deletion src/lib/types/misc.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { z } from 'zod';

export const newChannelSchema = z.object({
channelName: z.string().min(1, 'Channel name is required').max(24, 'Channel name cannot be longer than 24 characters.'),
channelName: z
.string()
.min(1, 'Channel name is required')
.max(24, 'Channel name cannot be longer than 24 characters.')
.refine((value) => !/^\d/.test(value), 'Channel name cannot start with a number.'),
});

export type NewChannelSchema = typeof newChannelSchema;
10 changes: 5 additions & 5 deletions src/lib/types/signup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,15 +7,15 @@ export const signupSchema = z
.string()
.min(3, 'Username must be at least 3 characters.')
.max(15, 'Username must be no more than 15 characters.')
.regex(/^(?![A-Z])/gm, 'Username cannot contain uppercase letters')
.regex(/^(?=[a-z0-9-_]+$)/gm, 'Username cannot contain special characters'),
.regex(/^(?![A-Z])/gm, 'Username cannot contain uppercase letters.')
.regex(/^(?=[a-z0-9-_]+$)/gm, 'Username cannot contain special characters.'),
password: z
.string()
.min(8, 'Password must be at least 8 characters.')
.regex(/(?=.*[A-Z])/gm, 'Password must contain at uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'Password must contain at lowercase letter.')
.regex(/(?=.*[A-Z])/gm, 'Password must contain an uppercase letter.')
.regex(/(?=.*[a-z])/gm, 'Password must contain a lowercase letter.')
.regex(/(?=.*\d)/gm, 'Password must contain at least one number.')
.regex(/(?=.*\W)/gm, 'Password must contain at least one special character'),
.regex(/(?=.*\W)/gm, 'Password must contain at least one special character.'),
verify: z.string().nonempty('Passwords do not match.'),
})
.refine((schema) => schema.password !== 'Password123!', {
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(main)/+layout.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ import { redirect } from '@sveltejs/kit';
import { superValidate } from 'sveltekit-superforms';
import { zod } from 'sveltekit-superforms/adapters';

interface Profile {
type Profile = {
username: string;
image: string;
}
};

export async function load({ request }) {
const session = await auth.api.getSession({
Expand Down
4 changes: 2 additions & 2 deletions src/routes/(main)/channel/[channel]/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import type { TypeMessage } from '$lib/types';
import { error, redirect } from '@sveltejs/kit';
import { auth } from '$lib/server/db/auth';

interface ChannelLoad {
type ChannelLoad = {
messages: TypeMessage[];
currentUserID: string;
currentUserName: string;
}
};

export async function load({ params, request }): Promise<ChannelLoad> {
const session = await auth.api.getSession({
Expand Down
14 changes: 14 additions & 0 deletions src/routes/(server)/api/checkauth/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { auth } from '$lib/server/db/auth';
import { json } from '@sveltejs/kit';

export const GET = async ({ request }) => {
const session = await auth.api.getSession({
headers: request.headers,
});

if (session) {
return json({ status: 200 });
} else {
return json({ status: 401 }, { status: 401 });
}
};
2 changes: 1 addition & 1 deletion src/routes/signup/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,6 @@ export const actions = {
return setError(form, 'verify', errorMessage.charAt(0).toUpperCase() + errorMessage.slice(1));
}

return message(form, 'Successfuly signed in.');
return message(form, 'Successfuly signed up.');
},
} satisfies Actions;
Binary file added static/freakybear.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading