From 70ce4d900ebae3c74f2ecc49423fa2546ae6dd45 Mon Sep 17 00:00:00 2001
From: Andrew Novac <16753077+novatorem@users.noreply.github.com>
Date: Sat, 28 Feb 2026 21:08:51 -0500
Subject: [PATCH 1/2] Improved backgrounds; upgraded queries; runes updates
---
.claude/settings.local.json | 19 +
.github/workflows/ci.yml | 31 +
README.md | 51 +-
package-lock.json | 398 ++++++--
package.json | 9 +-
src/app.css | 2 -
src/app.html | 7 +-
src/database.types.ts | 56 +-
src/hooks.server.ts | 31 +-
src/lib/dashboard/loader.ts | 40 +-
src/lib/friends/api.ts | 4 -
src/lib/friends/components/DeleteModal.svelte | 1 -
src/lib/friends/components/List.svelte | 63 +-
.../friends/components/ListSkeleton.svelte | 4 -
src/lib/friends/components/Requests.svelte | 892 +++++++++---------
.../components/RequestsSkeleton.svelte | 3 -
src/lib/friends/order.test.ts | 111 +++
src/lib/friends/order.ts | 20 -
src/lib/profile/api.ts | 2 +-
src/lib/profile/validation.test.ts | 112 +++
src/lib/profile/validation.ts | 8 +-
src/lib/realtime/subscriptions.ts | 68 +-
src/lib/status/components/Skeleton.svelte | 3 -
src/lib/status/formatting.test.ts | 64 ++
src/lib/status/formatting.ts | 11 -
src/lib/status/quick.test.ts | 110 +++
src/lib/status/quick.ts | 21 +-
src/lib/status/validation.test.ts | 22 +
src/lib/ui/DebugPanel.svelte | 21 -
src/lib/ui/DotGridBackground.svelte | 79 ++
src/lib/ui/Navigation.svelte | 5 -
src/lib/ui/RelativeTime.svelte | 24 +
src/lib/ui/Toast.svelte | 2 -
src/lib/ui/ToastContainer.svelte | 1 -
src/lib/ui/notifications.ts | 5 -
src/lib/ui/toast.ts | 10 +-
src/routes/+layout.svelte | 24 +-
src/routes/+layout.ts | 10 +-
src/routes/+page.svelte | 76 +-
src/routes/auth/+layout.svelte | 7 +-
src/routes/auth/+page.server.ts | 32 +-
src/routes/auth/+page.svelte | 788 ++++++++++------
src/routes/auth/confirm/+server.ts | 7 +-
src/routes/auth/error/+page.svelte | 55 +-
src/routes/auth/logout/+server.ts | 6 -
.../auth/reset-password/+page.server.ts | 9 +
src/routes/auth/reset-password/+page.svelte | 212 +++++
src/routes/dashboard/+layout.server.ts | 6 +-
src/routes/dashboard/+page.server.ts | 2 -
src/routes/dashboard/+page.svelte | 638 ++++++-------
src/routes/dashboard/settings/+page.svelte | 326 +++++--
static/favicon.png | Bin 1327597 -> 0 bytes
static/favicon.svg | 4 +
static/icon-192.png | Bin 0 -> 3729 bytes
static/icon-512.png | Bin 0 -> 14302 bytes
static/manifest.json | 26 +
vite.config.ts | 6 +-
57 files changed, 2894 insertions(+), 1650 deletions(-)
create mode 100644 .claude/settings.local.json
create mode 100644 .github/workflows/ci.yml
create mode 100644 src/lib/friends/order.test.ts
create mode 100644 src/lib/profile/validation.test.ts
create mode 100644 src/lib/status/formatting.test.ts
create mode 100644 src/lib/status/quick.test.ts
create mode 100644 src/lib/status/validation.test.ts
create mode 100644 src/lib/ui/DotGridBackground.svelte
create mode 100644 src/lib/ui/RelativeTime.svelte
create mode 100644 src/routes/auth/reset-password/+page.server.ts
create mode 100644 src/routes/auth/reset-password/+page.svelte
delete mode 100644 static/favicon.png
create mode 100644 static/favicon.svg
create mode 100644 static/icon-192.png
create mode 100644 static/icon-512.png
create mode 100644 static/manifest.json
diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..7e9ec47
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,19 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash(python3:*)",
+ "Bash(cd \"C:/Projects/rez\" && npm run check 2>&1)",
+ "Bash(cd \"C:/Projects/rez\" && npx svelte-kit sync && npx svelte-check --tsconfig ./tsconfig.json 2>&1)",
+ "Bash(cd \"C:/Projects/rez\" && node_modules/.bin/svelte-kit sync && node_modules/.bin/svelte-check --tsconfig ./tsconfig.json 2>&1)",
+ "Bash(which node:*)",
+ "Bash(tail:*)",
+ "Bash(cd \"C:/Projects/rez\" && node_modules/.bin/svelte-kit sync 2>&1 && node_modules/.bin/svelte-check --tsconfig ./tsconfig.json 2>&1)",
+ "Bash(cd C:/Projects/rez && node_modules/.bin/svelte-kit sync && node_modules/.bin/svelte-check --tsconfig ./tsconfig.json 2>&1)",
+ "Bash(cd C:/Projects/rez && npx --yes npm-check-updates --upgrade 2>&1)",
+ "Bash(cd C:/Projects/rez && npm install 2>&1)",
+ "Bash(cd C:/Projects/rez && npm audit 2>&1)",
+ "Bash(cd C:/Projects/rez && npm run build 2>&1)",
+ "Bash(npm view svelte-boring-avatars versions --json 2>&1 | python3 -c \"import sys,json; vs=json.load\\(sys.stdin\\); print\\('All versions:', vs[-5:]\\)\" && npm view svelte-boring-avatars dist-tags 2>&1)"
+ ]
+ }
+}
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..045bbf4
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,31 @@
+name: CI
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ check:
+ name: Check, Lint & Test
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 18
+ cache: npm
+
+ - run: npm ci
+
+ - name: Type check
+ run: npm run check
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Test
+ run: npm test
diff --git a/README.md b/README.md
index 1652999..16560d8 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ A real-time social status app where you can share what you're up to with your fr
## Overview
Rez lets you:
+
- Post a short status (up to 42 characters) so friends know what you're up to
- See your friends' statuses and when they last updated them
- Send, accept, and reject friend requests by username
@@ -55,12 +56,13 @@ Rez lets you:
| Feature | Details |
|---|---|
-| Authentication | Email/password with optional email confirmation |
+| Authentication | Email/password with email confirmation, password reset, email change |
| Status updates | 42-character limit, with quick-status presets |
| Friend requests | Send by username, accept/reject/cancel |
| Real-time sync | Supabase Realtime (optional, gracefully degrades) |
| Friend ordering | Drag-and-drop reorder, persisted in `localStorage` |
| Themes | 35 built-in DaisyUI themes, cookie-persisted |
+| PWA | Installable on mobile and desktop via web app manifest |
| Profile | Username (3–20 chars) + optional display name (up to 50 chars) |
| Data export | Download all your data as JSON |
| Account deletion | Password-confirmed, cascades all data including auth user |
@@ -80,8 +82,10 @@ Rez lets you:
| Avatars | `svelte-boring-avatars` (deterministic from user ID) |
| Theme switching | `theme-change` |
| Build tool | Vite 7 |
+| Testing | [Vitest](https://vitest.dev/) |
| Linting | ESLint 9 + `eslint-plugin-svelte` |
| Formatting | Prettier + `prettier-plugin-svelte` + `prettier-plugin-tailwindcss` |
+| CI | GitHub Actions (check, lint, test) |
---
@@ -131,6 +135,8 @@ npm run check # Type-check the project
npm run check:watch # Type-check in watch mode
npm run lint # Check formatting + ESLint
npm run format # Auto-format all files
+npm test # Run Vitest tests
+npm run test:watch # Run tests in watch mode
```
### Building for Production
@@ -200,7 +206,7 @@ In your Supabase dashboard under **Authentication → URL Configuration**:
- **Site URL**: your production domain (e.g. `https://yourapp.com`)
- **Redirect URLs**: add `https://yourapp.com/auth/confirm` (and `http://localhost:5173/auth/confirm` for local dev)
-Email confirmation is supported. When a new user signs up, they receive a confirmation email that redirects to `/auth/confirm`, which validates the OTP token and then redirects to `/dashboard`.
+The `/auth/confirm` endpoint handles multiple OTP types: signup confirmation, password recovery, and email change. For password resets, the recovery email links to `/auth/confirm?next=/auth/reset-password`, which verifies the token and redirects to the new-password form.
---
@@ -229,7 +235,7 @@ src/
│ │ ├── Requests.svelte # Send/accept/reject/cancel requests
│ │ └── RequestsSkeleton.svelte
│ ├── profile/
-│ │ ├── api.ts # findUserByUsername, checkUsernameAvailability
+│ │ ├── api.ts # checkUsernameAvailability
│ │ └── validation.ts # Username/display-name constants, validators, ERROR_MESSAGES
│ ├── realtime/
│ │ └── subscriptions.ts # RealtimeSubscriptionManager class
@@ -247,6 +253,7 @@ src/
│ ├── DebugPanel.svelte # Floating debug log panel (dev/iOS/error)
│ ├── Footer.svelte
│ ├── Navigation.svelte # Sticky nav with logout + settings link
+│ ├── RelativeTime.svelte # Live-ticking relative timestamp component
│ ├── ThemeSelect.svelte # Theme picker component
│ ├── Toast.svelte
│ └── ToastContainer.svelte
@@ -258,11 +265,14 @@ src/
│ ├── +page.svelte # Landing / hero page
│ ├── auth/
│ │ ├── +layout.svelte
-│ │ ├── +page.server.ts # login/signup form actions
-│ │ ├── +page.svelte # Login/signup tab UI
-│ │ ├── confirm/+server.ts # Email confirmation OTP handler
+│ │ ├── +page.server.ts # login/signup/forgotPassword form actions
+│ │ ├── +page.svelte # Login/signup tab UI with forgot password flow
+│ │ ├── confirm/+server.ts # Email confirmation OTP handler (signup, recovery, email change)
│ │ ├── error/+page.svelte # Auth error page
-│ │ └── logout/+server.ts # POST endpoint: server-side sign out + cookie clear
+│ │ ├── logout/+server.ts # POST endpoint: server-side sign out + cookie clear
+│ │ └── reset-password/
+│ │ ├── +page.server.ts # Load function (session check)
+│ │ └── +page.svelte # New password form after recovery
│ └── dashboard/
│ ├── +layout.server.ts # Auth guard - redirects unauthenticated users to /
│ ├── +layout.svelte # Dashboard layout with Navigation
@@ -270,7 +280,7 @@ src/
│ ├── +page.svelte # Main dashboard page
│ └── settings/
│ ├── +page.server.ts
-│ └── +page.svelte # Settings page
+│ └── +page.svelte # Settings page (profile, security, quick statuses, appearance, data)
│
└── sql/
├── functions/
@@ -292,7 +302,7 @@ src/
2. **`+layout.server.ts`** passes `session` and raw cookies to the client.
3. **`+layout.ts`** creates a Supabase browser client (or server client during SSR) and exposes `supabase`, `session`, and `user` to all child layouts/pages.
4. **Dashboard page** receives only `session` from the server - all dashboard data (friends, statuses, requests) is loaded client-side via `DashboardDataLoader`. This keeps server load minimal and avoids serializing large data structures through the load chain.
-5. **Real-time** updates are handled by `RealtimeSubscriptionManager`, which subscribes to `friend_requests`, `friends`, and `profiles` table changes. On any relevant event, a debounced refresh (300ms) reloads dashboard data.
+5. **Real-time** updates are handled by `RealtimeSubscriptionManager`, which subscribes to `friend_requests`, `friends`, and `profiles` table changes. Updates are granular: profile/status changes are patched in-place from the realtime payload (zero DB calls), friend request changes trigger a targeted reload of just the request arrays, and friendship changes (rare, structural) trigger a full data reload.
### Database Schema
@@ -323,8 +333,8 @@ Quick statuses and friend order are stored in the browser's `localStorage` rathe
**Svelte 5 runes throughout**
All components use Svelte 5's rune-based reactivity (`$state`, `$derived`, `$effect`, `$props`, `$bindable`). This is the modern Svelte 5 API and provides cleaner component logic with explicit reactivity.
-**Debounced real-time refresh**
-Rather than updating specific state slices on each Realtime event, the app does a full data reload with a 300ms debounce. This is simpler to reason about and handles batched events correctly, at the cost of slightly more database reads.
+**Granular real-time updates**
+Realtime events are handled with three strategies: profile/status changes are patched in-place directly from the event payload (zero database calls), friend request changes trigger a debounced (300ms) reload of just the request arrays (two small queries), and friendship changes trigger a full reload since they are rare structural events that affect the profiles subscription scope.
**`adapter-auto`**
Using `@sveltejs/adapter-auto` means the project deploys to any supported platform without configuration changes. Swap in a specific adapter if you need fine-grained control.
@@ -338,7 +348,10 @@ Using `@sveltejs/adapter-auto` means the project deploys to any supported platfo
- Auth uses Supabase's email/password flow via `@supabase/ssr`.
- Session validation in `hooks.server.ts` calls both `getSession()` and `getUser()`. The `getUser()` call makes a network request to Supabase to cryptographically verify the JWT - this prevents accepting a locally-forged or replayed token. This pattern follows [Supabase's security recommendations](https://supabase.com/docs/guides/auth/server-side/nextjs#understanding-what-session-means).
- `safeGetSession()` returns `{ session: null, user: null }` on any validation error rather than propagating the error upward.
-- The auth guard in `hooks.server.ts` redirects unauthenticated users away from `/dashboard/**` and redirects authenticated users away from `/auth`.
+- The auth guard has two layers: `hooks.server.ts` redirects unauthenticated users away from `/dashboard/**` to `/auth`, and `dashboard/+layout.server.ts` provides a secondary guard that redirects to `/`. Authenticated users hitting exactly `/auth` are redirected to `/dashboard` (sub-routes like `/auth/reset-password` are not affected).
+- **Password reset**: Users request a reset link from the login page via `resetPasswordForEmail()`. The email links through `/auth/confirm` (OTP verification) and redirects to `/auth/reset-password` where they set a new password via `updateUser()`. The root layout's `onAuthStateChange` listener also handles the `PASSWORD_RECOVERY` event as a safety redirect.
+- **Password change**: Authenticated users can change their password from Settings. The current password is verified via `signInWithPassword()` re-authentication before calling `updateUser()`.
+- **Email change**: Authenticated users can request an email change from Settings via `updateUser({ email })`. Supabase sends a confirmation link to the new address; the existing `/auth/confirm` handler processes the `email_change` OTP type.
- Logout hits both the client-side `supabase.auth.signOut()` and a server-side `POST /auth/logout` endpoint that manually clears all Supabase auth cookies.
### Row Level Security
@@ -350,20 +363,23 @@ All four public tables have RLS enabled. The policies enforce:
| `users` | All authenticated users can read | - | Own row only | Own row only |
| `profiles` | All authenticated users can read | Own row only | Own row only | - |
| `friends` | Only if `user_id` or `friend_id` matches | Only if `user_id` or `friend_id` matches | - | Only if `user_id` or `friend_id` matches |
-| `friend_requests` | Own rows (as requester or target) | Own as requester | Own as target | Own as requester or target |
+| `friend_requests` | Own rows (as requester or target) | Own as requester (rate-limited) | Own as target | Own as requester or target |
The read-all policy on `users` and `profiles` is intentional - users need to search for others by username and view friends' statuses.
+**Rate limiting**: A restrictive RLS policy on `friend_requests` limits each user to 20 INSERT operations per hour. This is enforced at the database level via a `RESTRICTIVE` policy that is ANDed with the permissive INSERT policy, backed by a composite index on `(requester_id, created_at)`.
+
### Cookie Security
Auth session cookies are set with:
+
- **`path: '/'`** - applies app-wide
- **`secure: true`** in production (HTTPS only) - prevents cookie theft over plain HTTP
- **`sameSite: 'lax'`** - blocks cross-site POST requests from sending the cookie (CSRF mitigation)
- **`domain`** set to the request hostname in production - prevents session sharing across subdomains
- Localhost/127.0.0.1: `domain` is omitted to avoid browser quirks
-iOS Safari receives `sameSite: 'lax'` (same as all other clients), which avoids the more restrictive `none` behavior that can cause issues with in-app browsers.
+All clients (including iOS Safari) receive `sameSite: 'lax'`, which avoids the more restrictive `none` behavior that can cause issues with in-app browsers.
### Database Functions
@@ -418,10 +434,5 @@ These are purely client-side and device-specific - they are not synced to the da
## Potential Improvements
**API**
-- Introduce an API. View only, so people can make their own dashboards (e.g. TRMNL).
-
-**User Experience**
-- No password reset or email change flow is exposed in the UI.
-**Code Quality**
-- No automated tests (unit or integration).
+- Introduce an API. View only, so people can make their own dashboards (e.g. TRMNL).
diff --git a/package-lock.json b/package-lock.json
index 0307c6a..3e93c67 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,8 +20,6 @@
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.53.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
- "@tailwindcss/forms": "^0.5.11",
- "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.1",
"eslint": "^9.39.3",
"eslint-config-prettier": "^10.1.8",
@@ -35,7 +33,19 @@
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
- "vite": "^7.3.1"
+ "vite": "^7.3.1",
+ "vitest": "^4.0.18"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.8.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
+ "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
@@ -1298,19 +1308,6 @@
"vite": "^6.3.0 || ^7.0.0"
}
},
- "node_modules/@tailwindcss/forms": {
- "version": "0.5.11",
- "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz",
- "integrity": "sha512-h9wegbZDPurxG22xZSoWtdzc41/OlNEUQERNqI/0fOwa2aVlWGu7C35E/x6LDyD3lgtztFSSjKZyuVM0hxhbgA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "mini-svg-data-uri": "^1.2.3"
- },
- "peerDependencies": {
- "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1"
- }
- },
"node_modules/@tailwindcss/node": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
@@ -1568,19 +1565,6 @@
"node": ">= 20"
}
},
- "node_modules/@tailwindcss/typography": {
- "version": "0.5.19",
- "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.19.tgz",
- "integrity": "sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "postcss-selector-parser": "6.0.10"
- },
- "peerDependencies": {
- "tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
- }
- },
"node_modules/@tailwindcss/vite": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
@@ -1596,6 +1580,17 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
"node_modules/@types/cookie": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz",
@@ -1603,6 +1598,13 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1930,6 +1932,117 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/@vitest/expect": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.0.18.tgz",
+ "integrity": "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.0.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "chai": "^6.2.1",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.0.18.tgz",
+ "integrity": "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.0.18",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.0.18.tgz",
+ "integrity": "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.0.18.tgz",
+ "integrity": "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.0.18",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.0.18.tgz",
+ "integrity": "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.0.18.tgz",
+ "integrity": "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.0.18.tgz",
+ "integrity": "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.0.18",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2003,6 +2116,16 @@
"node": ">= 0.4"
}
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
@@ -2041,6 +2164,16 @@
"node": ">=6"
}
},
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -2194,9 +2327,9 @@
}
},
"node_modules/detect-libc": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
- "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
@@ -2224,6 +2357,13 @@
"node": ">=10.13.0"
}
},
+ "node_modules/es-module-lexer": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+ "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/esbuild": {
"version": "0.27.3",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.3.tgz",
@@ -2503,6 +2643,16 @@
"node": ">=4.0"
}
},
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
@@ -2513,6 +2663,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
@@ -3143,16 +3303,6 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
- "node_modules/mini-svg-data-uri": {
- "version": "1.4.4",
- "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz",
- "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "mini-svg-data-uri": "cli.js"
- }
- },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3313,6 +3463,13 @@
"node": ">=8"
}
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -3456,20 +3613,6 @@
"postcss": "^8.4.29"
}
},
- "node_modules/postcss-selector-parser": {
- "version": "6.0.10",
- "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
- "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "cssesc": "^3.0.0",
- "util-deprecate": "^1.0.2"
- },
- "engines": {
- "node": ">=4"
- }
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -3718,6 +3861,13 @@
"node": ">=8"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/sirv": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz",
@@ -3743,6 +3893,20 @@
"node": ">=0.10.0"
}
},
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "3.10.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz",
+ "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -3899,6 +4063,23 @@
"integrity": "sha512-B/UdsgdHAGhSKHTAQnxg/etN0RaMDpehuJmZIjLMDVJ6DGIliRHGD6pODi1CXLQAN9GV0GSyB3G6yCuK05PkPQ==",
"license": "MIT"
},
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz",
+ "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
@@ -3916,6 +4097,16 @@
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
+ "node_modules/tinyrainbow": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.0.3.tgz",
+ "integrity": "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/totalist": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz",
@@ -4114,6 +4305,84 @@
}
}
},
+ "node_modules/vitest": {
+ "version": "4.0.18",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.0.18.tgz",
+ "integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.0.18",
+ "@vitest/mocker": "4.0.18",
+ "@vitest/pretty-format": "4.0.18",
+ "@vitest/runner": "4.0.18",
+ "@vitest/snapshot": "4.0.18",
+ "@vitest/spy": "4.0.18",
+ "@vitest/utils": "4.0.18",
+ "es-module-lexer": "^1.7.0",
+ "expect-type": "^1.2.2",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^3.10.0",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.0.18",
+ "@vitest/browser-preview": "4.0.18",
+ "@vitest/browser-webdriverio": "4.0.18",
+ "@vitest/ui": "4.0.18",
+ "happy-dom": "*",
+ "jsdom": "*"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -4130,6 +4399,23 @@
"node": ">= 8"
}
},
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
diff --git a/package.json b/package.json
index 6e75cc1..d19d1ad 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,9 @@
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"format": "prettier --write .",
- "lint": "prettier --check . && eslint ."
+ "lint": "prettier --check . && eslint .",
+ "test": "vitest run",
+ "test:watch": "vitest"
},
"devDependencies": {
"@eslint/compat": "^1.4.1",
@@ -19,8 +21,6 @@
"@sveltejs/adapter-auto": "^7.0.1",
"@sveltejs/kit": "^2.53.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
- "@tailwindcss/forms": "^0.5.11",
- "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.2.1",
"eslint": "^9.39.3",
"eslint-config-prettier": "^10.1.8",
@@ -34,7 +34,8 @@
"tailwindcss": "^4.2.1",
"typescript": "^5.9.3",
"typescript-eslint": "^8.56.1",
- "vite": "^7.3.1"
+ "vite": "^7.3.1",
+ "vitest": "^4.0.18"
},
"dependencies": {
"@supabase/ssr": "^0.8.0",
diff --git a/src/app.css b/src/app.css
index 5676928..da6e77a 100644
--- a/src/app.css
+++ b/src/app.css
@@ -1,6 +1,4 @@
@import 'tailwindcss';
-@plugin '@tailwindcss/forms';
-@plugin '@tailwindcss/typography';
@plugin "daisyui" {
themes: all;
}
diff --git a/src/app.html b/src/app.html
index f2516ae..1093ed8 100644
--- a/src/app.html
+++ b/src/app.html
@@ -2,8 +2,13 @@
-
+
+
+
+
+
+
%sveltekit.head%
diff --git a/src/database.types.ts b/src/database.types.ts
index 69b5250..49455d6 100644
--- a/src/database.types.ts
+++ b/src/database.types.ts
@@ -51,31 +51,28 @@ export interface Database {
};
Relationships: [];
};
- friend_requests: {
- Row: {
- id: string;
- requester_id: string;
- target_id: string;
- status: string;
- created_at: string;
- updated_at: string;
- };
- Insert: {
- id?: string;
- requester_id: string;
- target_id: string;
- status?: string;
- created_at?: string;
- updated_at?: string;
- };
- Update: {
- id?: string;
- requester_id?: string;
- target_id?: string;
- status?: string;
- created_at?: string;
- updated_at?: string;
- };
+ friend_requests: {
+ Row: {
+ id: string;
+ requester_id: string;
+ target_id: string;
+ created_at: string;
+ updated_at: string;
+ };
+ Insert: {
+ id?: string;
+ requester_id: string;
+ target_id: string;
+ created_at?: string;
+ updated_at?: string;
+ };
+ Update: {
+ id?: string;
+ requester_id?: string;
+ target_id?: string;
+ created_at?: string;
+ updated_at?: string;
+ };
Relationships: [
{
foreignKeyName: 'friend_requests_requester_id_fkey';
@@ -130,7 +127,14 @@ export interface Database {
[_ in never]: never;
};
Functions: {
- [_ in never]: never;
+ get_dashboard_data: {
+ Args: Record;
+ Returns: Json;
+ };
+ delete_user_account: {
+ Args: Record;
+ Returns: undefined;
+ };
};
Enums: {
[_ in never]: never;
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index 6b0c0d8..de0b48b 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -1,5 +1,3 @@
-// Set NODE_TLS_REJECT_UNAUTHORIZED=0 only in development mode
-// This allows self-signed certificates to be accepted
if (process.env.NODE_ENV === 'development') {
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
console.log('⚠️\tSSL certificate verification disabled for development');
@@ -12,21 +10,17 @@ import { type Handle, redirect } from '@sveltejs/kit';
import { sequence } from '@sveltejs/kit/hooks';
import https from 'node:https';
-// Create a custom agent that allows self-signed certificates in development
const isDev = process.env.NODE_ENV === 'development';
const httpsAgent = isDev ? new https.Agent({ rejectUnauthorized: false }) : undefined;
-// Custom fetch function that uses our HTTPS agent in development
const customFetch = (input: URL | RequestInfo, init?: RequestInit) => {
if (isDev) {
- // In development, use node-fetch with our custom agent
return fetch(input, {
...init,
// @ts-expect-error - Agent is not in standard RequestInit but works with Node.js fetch
agent: httpsAgent
});
}
- // In production, use the standard fetch
return fetch(input, init);
};
@@ -38,33 +32,13 @@ const supabase: Handle = async ({ event, resolve }) => {
cookiesToSet: { name: string; value: string; options?: Record }[]
) => {
cookiesToSet.forEach(({ name, value, options }) => {
- // Get the current host from the request
const host = event.url.hostname;
- // Detect iOS Safari user agent for special cookie handling
- const userAgent = event.request.headers.get('user-agent') || '';
- const isIOS = /iPad|iPhone|iPod/.test(userAgent);
- const isSafari = /Safari/.test(userAgent) && !/Chrome|CriOS|FxiOS/.test(userAgent);
- const isIOSSafari = isIOS && isSafari;
-
- // SECURITY FIX: Set secure cookie options to prevent session sharing across devices/domains
- // This addresses the critical vulnerability where users were sharing sessions
- // iOS Safari compatibility: Use 'none' with secure for cross-site, or 'lax' for same-site
- // For iOS Safari, we need to be more careful with sameSite settings
const secureOptions = {
...options,
path: '/',
- // Note: We don't set httpOnly for auth tokens as Supabase needs client-side access
- // Set secure flag in production (requires HTTPS)
secure: event.url.protocol === 'https:',
- // iOS Safari fix: Use 'lax' for same-site, but ensure secure is set properly
- // For iOS Safari, 'lax' works better than 'none' for same-site requests
- sameSite: (isIOSSafari && event.url.protocol === 'https:')
- ? 'lax' as const
- : 'lax' as const,
- // Set domain to current host to prevent cross-domain sharing
- // This ensures sessions are isolated per domain/device
- // iOS Safari: Don't set domain for localhost, and be careful with subdomains
+ sameSite: 'lax' as const,
domain: host.startsWith('localhost') || host.startsWith('127.0.0.1')
? undefined
: host
@@ -95,7 +69,6 @@ const supabase: Handle = async ({ event, resolve }) => {
} = await event.locals.supabase.auth.getUser();
if (userError) {
- // JWT validation has failed - following official docs pattern
return { session: null, user: null };
}
@@ -125,9 +98,7 @@ const authGuard: Handle = async ({ event, resolve }) => {
return resolve(event);
};
-// Handle Chrome DevTools requests to prevent 404 errors
const handleDevToolsRequests: Handle = async ({ event, resolve }) => {
- // Check if the request is for Chrome DevTools specific endpoints
if (event.url.pathname.includes('/.well-known/appspecific/com.chrome.devtools')) {
return new Response(JSON.stringify({ message: 'Not implemented' }), {
status: 200,
diff --git a/src/lib/dashboard/loader.ts b/src/lib/dashboard/loader.ts
index 2113a25..231085b 100644
--- a/src/lib/dashboard/loader.ts
+++ b/src/lib/dashboard/loader.ts
@@ -2,7 +2,6 @@ import type { SupabaseClient } from '@supabase/supabase-js';
import type { Database } from '../../database.types';
import { getQuickStatuses, type QuickStatus } from '../status/quick.js';
-// Types for the dashboard data
export interface FriendRequest {
id: string;
requester_id: string;
@@ -10,7 +9,6 @@ export interface FriendRequest {
requester_display_name: string | null;
}
-// Types for joined queries
interface FriendRequestWithUser {
id: string;
requester_id: string;
@@ -80,11 +78,9 @@ export interface UserExportData {
};
}
-// Utility function to remove duplicates by ID
const deduplicateById = (items: T[]): T[] =>
items.filter((item, index, self) => index === self.findIndex((other) => other.id === item.id));
-// Shape of the JSON object returned by the get_dashboard_data() RPC function.
interface DashboardRpcResult {
username: string;
display_name: string | null;
@@ -120,23 +116,6 @@ export class DashboardDataLoader {
this.userId = userId;
}
- async loadUserProfile(): Promise<{
- username: string;
- display_name: string | null;
- status: string;
- }> {
- const [userData, profileData] = await Promise.all([
- this.supabase.from('users').select('username, display_name').eq('id', this.userId).single(),
- this.supabase.from('profiles').select('status').eq('id', this.userId).single()
- ]);
-
- return {
- username: userData.data?.username || '',
- display_name: userData.data?.display_name || null,
- status: profileData.data?.status || ''
- };
- }
-
async loadFriendRequests(): Promise {
const { data } = await this.supabase
.from('friend_requests')
@@ -179,8 +158,6 @@ export class DashboardDataLoader {
}
async loadFriends(): Promise {
- // Get all friendships where current user is either user_id or friend_id
- // Since we now store only one record per friendship, we need to check both columns
const { data: friendships } = await this.supabase
.from('friends')
.select('id, user_id, friend_id')
@@ -190,31 +167,26 @@ export class DashboardDataLoader {
return [];
}
- // Get friend IDs (the other person in each friendship)
const friendIds = friendships.map((friendship) => {
return friendship.user_id === this.userId ? friendship.friend_id : friendship.user_id;
});
- // Fetch user details for all friends
const { data: friendUsers } = await this.supabase
.from('users')
.select('id, username, display_name')
.in('id', friendIds);
- // Get statuses for all friends in parallel
const { data: friendStatuses } =
friendIds.length > 0
? await this.supabase.from('profiles').select('id, status, updated_at').in('id', friendIds)
: { data: [] };
- // Create maps for friend data
const userMap = new Map(friendUsers?.map((user) => [user.id, user]) || []);
const statusMap = new Map(friendStatuses?.map((profile) => [profile.id, profile.status]) || []);
const statusUpdatedAtMap = new Map(
friendStatuses?.map((profile) => [profile.id, profile.updated_at]) || []
);
- // Process and format friends
const formattedFriends = friendIds.map((friendId) => {
const user = userMap.get(friendId);
return {
@@ -230,14 +202,11 @@ export class DashboardDataLoader {
}
async loadQuickStatuses(): Promise {
- // Load quick statuses from localStorage instead of database
return getQuickStatuses();
}
async loadAllData(): Promise {
- const rpcResult = await this.supabase.rpc(
- 'get_dashboard_data' as keyof Database['public']['Functions']
- );
+ const rpcResult = await this.supabase.rpc('get_dashboard_data');
if (rpcResult.error) {
throw rpcResult.error;
@@ -260,7 +229,6 @@ export class DashboardDataLoader {
}
async exportUserData(): Promise {
- // Load all user data including detailed user and profile information
const [userData, profileData, friendRequests, sentFriendRequests, friends, quickStatuses] =
await Promise.all([
this.supabase
@@ -309,12 +277,8 @@ export class DashboardDataLoader {
}
async deleteUserAccount(): Promise {
- // Use the database function to delete the user account
- // This function handles all the deletion logic and can delete the auth user
try {
- const { error } = await this.supabase.rpc(
- 'delete_user_account' as keyof Database['public']['Functions']
- );
+ const { error } = await this.supabase.rpc('delete_user_account');
if (error) {
throw new Error(`Failed to delete account: ${error.message}`);
diff --git a/src/lib/friends/api.ts b/src/lib/friends/api.ts
index fd05bca..4a2f2ff 100644
--- a/src/lib/friends/api.ts
+++ b/src/lib/friends/api.ts
@@ -1,13 +1,11 @@
import type { SupabaseClient } from '@supabase/supabase-js';
-// Friendship verification utility
export async function verifyFriendshipExists(
supabase: SupabaseClient,
userId: string,
friendId: string
): Promise {
try {
- // Check for the single friendship record (could be in either direction)
const { data: friendship, error } = await supabase
.from('friends')
.select('id')
@@ -28,7 +26,6 @@ export async function verifyFriendshipExists(
}
}
-// Friend request utilities
export async function checkExistingFriendRequest(
supabase: SupabaseClient,
requesterId: string,
@@ -73,7 +70,6 @@ export async function checkIncomingFriendRequest(
throw error;
}
- // If a request exists, it's pending (requests are deleted when accepted/rejected)
return {
exists: !!incomingRequest,
isPending: !!incomingRequest
diff --git a/src/lib/friends/components/DeleteModal.svelte b/src/lib/friends/components/DeleteModal.svelte
index 9985b37..bb0ada0 100644
--- a/src/lib/friends/components/DeleteModal.svelte
+++ b/src/lib/friends/components/DeleteModal.svelte
@@ -11,7 +11,6 @@
$props();
-