privacy mode (#22)

This commit is contained in:
Aidan Bleser 2025-06-18 10:30:12 -05:00 committed by GitHub
parent 954b7173f7
commit d83cd134ff
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 182 additions and 9 deletions

View file

@ -2,12 +2,13 @@ import { betterAuth } from 'better-auth';
import { convexAdapter } from '@better-auth-kit/convex';
import { ConvexHttpClient } from 'convex/browser';
import 'dotenv/config';
import { api } from './backend/convex/_generated/api';
const convexClient = new ConvexHttpClient(process.env.PUBLIC_CONVEX_URL!);
const client = new ConvexHttpClient(process.env.PUBLIC_CONVEX_URL!);
export const auth = betterAuth({
secret: process.env.BETTER_AUTH_SECRET!,
database: convexAdapter(convexClient),
database: convexAdapter(client),
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
@ -17,7 +18,12 @@ export const auth = betterAuth({
databaseHooks: {
user: {
create: {
after: async (_user) => {},
after: async (user) => {
// create user settings
await client.mutation(api.user_settings.create, {
user_id: user.id,
});
},
},
},
},

View file

@ -14,6 +14,10 @@ export type MessageRole = Infer<typeof messageRoleValidator>;
export const ruleAttachValidator = v.union(v.literal('always'), v.literal('manual'));
export default defineSchema({
user_settings: defineTable({
user_id: v.string(),
privacy_mode: v.boolean(),
}).index('by_user', ['user_id']),
user_keys: defineTable({
user_id: v.string(),
provider: providerValidator,

View file

@ -0,0 +1,74 @@
import { internal } from './_generated/api';
import { query } from './_generated/server';
import { SessionObj } from './betterAuth';
import { mutation } from './functions';
import { v } from 'convex/values';
export const get = query({
args: {
session_token: v.string(),
},
handler: async (ctx, args) => {
const session = await ctx.runQuery(internal.betterAuth.getSession, {
sessionToken: args.session_token,
});
if (!session) {
throw new Error('Invalid session token');
}
const s = session as SessionObj;
return await ctx.db
.query('user_settings')
.withIndex('by_user', (q) => q.eq('user_id', s.userId))
.first();
},
});
export const set = mutation({
args: {
privacy_mode: v.boolean(),
session_token: v.string(),
},
handler: async (ctx, args) => {
const session = await ctx.runQuery(internal.betterAuth.getSession, {
sessionToken: args.session_token,
});
if (!session) {
throw new Error('Invalid session token');
}
const s = session as SessionObj;
const existing = await ctx.db
.query('user_settings')
.withIndex('by_user', (q) => q.eq('user_id', s.userId))
.first();
if (!existing) {
await ctx.db.insert('user_settings', {
user_id: s.userId,
privacy_mode: args.privacy_mode,
});
} else {
await ctx.db.patch(existing._id, {
privacy_mode: args.privacy_mode,
});
}
},
});
/** Never call this from the client */
export const create = mutation({
args: {
user_id: v.string(),
},
handler: async (ctx, args) => {
await ctx.db.insert('user_settings', {
user_id: args.user_id,
privacy_mode: false,
});
},
});

View file

@ -49,6 +49,10 @@
await goto(`/chat`);
}
const settings = useCachedQuery(api.user_settings.get, {
session_token: session.current?.session.token ?? '',
});
const conversationsQuery = useCachedQuery(api.conversations.get, {
session_token: session.current?.session.token ?? '',
});
@ -232,7 +236,13 @@
<Button href="/account" variant="ghost" class="h-auto w-full justify-start">
<Avatar src={page.data.session?.user.image ?? undefined}>
{#snippet children(avatar)}
<img {...avatar.image} alt="Your avatar" class="size-10 rounded-full" />
<img
{...avatar.image}
alt="Your avatar"
class={cn('size-10 rounded-full', {
'blur-[6px]': settings.data?.privacy_mode,
})}
/>
<span {...avatar.fallback} class="size-10 rounded-full">
{page.data.session?.user.name
.split(' ')
@ -242,8 +252,16 @@
{/snippet}
</Avatar>
<div class="flex flex-col">
<span class="text-sm">{page.data.session?.user.name}</span>
<span class="text-muted-foreground text-xs">{page.data.session?.user.email}</span>
<span class={cn('text-sm', { 'blur-[6px]': settings.data?.privacy_mode })}>
{page.data.session?.user.name}
</span>
<span
class={cn('text-muted-foreground text-xs', {
'blur-[6px]': settings.data?.privacy_mode,
})}
>
{page.data.session?.user.email}
</span>
</div>
</Button>
{:else}

View file

@ -8,9 +8,17 @@
import { Avatar } from 'melt/components';
import { Kbd } from '$lib/components/ui/kbd/index.js';
import { cmdOrCtrl } from '$lib/hooks/is-mac.svelte.js';
import { useCachedQuery } from '$lib/cache/cached-query.svelte.js';
import { session } from '$lib/state/session.svelte.js';
import { api } from '$lib/backend/convex/_generated/api.js';
import { cn } from '$lib/utils/utils.js';
let { data, children } = $props();
const settings = useCachedQuery(api.user_settings.get, {
session_token: session.current?.session.token ?? '',
});
const navigation: { title: string; href: string }[] = [
{
title: 'Account',
@ -69,7 +77,13 @@
<div class="flex flex-col place-items-center gap-2">
<Avatar src={data.session.user.image ?? undefined}>
{#snippet children(avatar)}
<img {...avatar.image} alt="Your avatar" class="size-40 rounded-full" />
<img
{...avatar.image}
alt="Your avatar"
class={cn('size-40 rounded-full', {
'blur-[20px]': settings.data?.privacy_mode,
})}
/>
<span {...avatar.fallback}>
{data.session.user.name
.split(' ')
@ -79,8 +93,20 @@
{/snippet}
</Avatar>
<div class="flex flex-col gap-1">
<p class="text-center text-2xl font-bold">{data.session.user.name}</p>
<span class="text-muted-foreground text-center text-sm">{data.session.user.email}</span>
<p
class={cn('text-center text-2xl font-bold', {
'blur-[6px]': settings.data?.privacy_mode,
})}
>
{data.session.user.name}
</p>
<span
class={cn('text-muted-foreground text-center text-sm', {
'blur-[6px]': settings.data?.privacy_mode,
})}
>
{data.session.user.email}
</span>
</div>
<div class="mt-4 flex w-full flex-col gap-2">
<span class="text-sm font-medium">Keyboard Shortcuts</span>

View file

@ -0,0 +1,45 @@
<script lang="ts">
import { api } from '$lib/backend/convex/_generated/api';
import { useCachedQuery } from '$lib/cache/cached-query.svelte';
import { session } from '$lib/state/session.svelte';
import { ResultAsync } from 'neverthrow';
import { useConvexClient } from 'convex-svelte';
import { Switch } from '$lib/components/ui/switch';
const client = useConvexClient();
const settings = useCachedQuery(api.user_settings.get, {
session_token: session.current?.session.token ?? '',
});
let privacyMode = $derived(settings.data?.privacy_mode ?? false);
async function toggleEnabled(v: boolean) {
privacyMode = v; // Optimistic!
if (!session.current?.user.id) return;
const res = await ResultAsync.fromPromise(
client.mutation(api.user_settings.set, {
privacy_mode: v,
session_token: session.current?.session.token,
}),
(e) => e
);
if (res.isErr()) privacyMode = !v; // Should have been a realist :(
}
</script>
<svelte:head>
<title>Account | Thom.chat</title>
</svelte:head>
<h1 class="text-2xl font-bold">Account Settings</h1>
<h2 class="text-muted-foreground mt-2 text-sm">Configure the settings for your account.</h2>
<div class="mt-4 flex flex-col gap-2">
<div class="flex place-items-center justify-between">
<span>Hide Personal Information</span>
<Switch bind:value={() => privacyMode, toggleEnabled} />
</div>
</div>