Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
60347be
add users table
ingoau Sep 19, 2025
477aab4
move to another file
ingoau Sep 19, 2025
6a3b907
add jwt payload type
ingoau Sep 19, 2025
85ccbf4
add clerkid to schema
ingoau Sep 19, 2025
69e555b
add indexing by clerk id
ingoau Sep 19, 2025
6a73f04
add code to get and update user
ingoau Sep 19, 2025
9a7663d
make user an id
ingoau Sep 19, 2025
31452da
change some stuff
ingoau Sep 19, 2025
af0e474
use userid
ingoau Sep 19, 2025
71337a5
add get user info function
ingoau Sep 19, 2025
a2996e9
use new function and stuff
ingoau Sep 19, 2025
372da0e
add optional fields
ingoau Sep 19, 2025
caf4512
change some settings for clerk
ingoau Sep 20, 2025
7c5e643
create account route
ingoau Sep 20, 2025
7989196
remove dialog
ingoau Sep 20, 2025
a455402
add active state for profile button
ingoau Sep 20, 2025
a1b827f
add sync settings store
ingoau Sep 20, 2025
78a0f32
add sync item
ingoau Sep 20, 2025
9534846
grey out icon when disabled
ingoau Sep 20, 2025
5909522
add syncing state
ingoau Sep 20, 2025
1302f57
add sync state to sidebar
ingoau Sep 20, 2025
dd67dc4
add animation when syncing
ingoau Sep 20, 2025
36ac954
add checkboxes
ingoau Sep 20, 2025
9a20581
add more sync settings
ingoau Sep 20, 2025
2ab1a0c
add more sync settings
ingoau Sep 20, 2025
a36b95b
bind settings to local storage
ingoau Sep 20, 2025
be88fa9
transition no matter the state
ingoau Sep 20, 2025
abf8599
disabled settings when sync is disabled
ingoau Sep 20, 2025
52f81b4
fix non existent user handling
ingoau Sep 20, 2025
7447775
fix verification
ingoau Sep 20, 2025
231edf8
remove unused analytics setting
ingoau Sep 20, 2025
54adb8c
add schema for settings
ingoau Sep 20, 2025
449944a
add favourites and history
ingoau Sep 20, 2025
969fb22
make sync things optional
ingoau Sep 20, 2025
289447b
add sync get query
ingoau Sep 20, 2025
ec2a7bc
add mutation for sync update
ingoau Sep 20, 2025
b01637f
change branch name thing
ingoau Sep 20, 2025
7268603
add functions for sync
ingoau Sep 21, 2025
25b7685
make functions async and change syncstate
ingoau Sep 21, 2025
f0b393c
change some stuff
ingoau Sep 21, 2025
ec742fa
move everything to one sync function
ingoau Sep 21, 2025
21ddbf6
fix issue with svelte
ingoau Sep 21, 2025
135a72e
fix issue with users that still have analytics key in local stoarge
ingoau Sep 21, 2025
8235588
add slightly better error handling
ingoau Sep 21, 2025
c5c6a88
add code to sync settings
ingoau Sep 21, 2025
450215d
add handler for settings change
ingoau Sep 21, 2025
bd34242
move analytics and sync to settings change function
ingoau Sep 21, 2025
74f308e
pass convex client down
ingoau Sep 21, 2025
8a84cde
disable sync for now
ingoau Sep 23, 2025
da14aa9
fix issue
ingoau Sep 23, 2025
545b726
Merge pull request #524 from EducationalTools/510-settings-sync-with-…
ingoau Sep 23, 2025
829fd21
use the same workflow for build
ingoau Sep 23, 2025
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
7 changes: 3 additions & 4 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,15 @@ on:
jobs:
build:
runs-on: ubuntu-latest
container:
image: 'archlinux:latest'
permissions: write-all

steps:
- name: Checkout code
uses: actions/checkout@v2

- name: Install packages
run: pacman -Sy pnpm nodejs npm chromium icu --noconfirm
- uses: pnpm/action-setup@v4
with:
version: 9

- name: Install dependencies
run: |
Expand Down
6 changes: 6 additions & 0 deletions src/convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import type {
FunctionReference,
} from "convex/server";
import type * as backups from "../backups.js";
import type * as sync from "../sync.js";
import type * as types from "../types.js";
import type * as utils from "../utils.js";

/**
* A utility for referencing Convex functions in your app's API.
Expand All @@ -25,6 +28,9 @@ import type * as backups from "../backups.js";
*/
declare const fullApi: ApiFromModules<{
backups: typeof backups;
sync: typeof sync;
types: typeof types;
utils: typeof utils;
}>;
export declare const api: FilterApi<
typeof fullApi,
Expand Down
37 changes: 16 additions & 21 deletions src/convex/backups.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import * as jose from 'jose';

// Shared helper function to verify JWT and return payload
async function verifyJwtAndGetPayload(jwt: string) {
if (!process.env.CLERK_JWT_KEY) {
throw new Error('Missing CLERK_JWT_KEY environment variable');
}
const publicKey = await jose.importSPKI(process.env.CLERK_JWT_KEY, 'RS256');
if (jwt.length === 0) {
throw new Error('Missing JWT');
}
const { payload } = await jose.jwtVerify(jwt, publicKey, {});
if (!payload.sub) {
throw new Error('Invalid JWT');
}
return payload;
}
import { getAndUpdateUser, getUser, verifyJwtAndGetPayload } from './utils';

export const get = query({
args: {
jwt: v.string()
},
handler: async (ctx, args) => {
const payload = await verifyJwtAndGetPayload(args.jwt);
const userInfo = await getUser(ctx, payload);
if (!userInfo) {
return [];
}
const backups = await ctx.db
.query('backup')
.order('desc')
.filter((q) => q.eq(q.field('user'), payload.sub))
.take(100);
.filter((q) => q.eq(q.field('user'), userInfo._id))
.collect();
return backups.map((backup) => ({
name: backup.name,
data: backup.data,
Expand All @@ -49,8 +37,12 @@ export const create = mutation({
if (!payload.sub) {
throw new Error('Invalid JWT: missing subject');
}
const userInfo = await getAndUpdateUser(ctx, payload);
if (!userInfo?._id) {
throw new Error('Something went wrong');
}
await ctx.db.insert('backup', {
user: payload.sub,
user: userInfo?._id,
name: args.name,
data: args.data
});
Expand All @@ -65,9 +57,12 @@ export const remove = mutation({
handler: async (ctx, args) => {
const payload = await verifyJwtAndGetPayload(args.jwt);
const backup = await ctx.db.get(args.id);
if (backup?.user !== payload.sub) {
const userInfo = await getAndUpdateUser(ctx, payload);

if (backup?.user !== userInfo?._id) {
throw new Error('Unauthorized');
}
await getAndUpdateUser(ctx, payload);
await ctx.db.delete(args.id);
}
});
36 changes: 33 additions & 3 deletions src/convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,41 @@ export default defineSchema({
comments: defineTable({
body: v.string(),
gmaeid: v.string(),
user: v.string()
user: v.id('users')
}),
backup: defineTable({
name: v.string(),
data: v.string(),
user: v.string()
})
user: v.id('users')
}),
users: defineTable({
email: v.string(),
firstName: v.optional(v.string()),
lastName: v.optional(v.string()),
avatar: v.optional(v.string()),
username: v.string(),
verified: v.boolean(),
clerkId: v.string(),
settings: v.optional(
v.object({
experimentalFeatures: v.boolean(),
open: v.string(),
theme: v.string(),
panic: v.object({
enabled: v.boolean(),
key: v.string(),
url: v.string(),
disableExperimentalMode: v.boolean()
}),
cloak: v.object({
mode: v.string(),
name: v.string(),
icon: v.string()
}),
history: v.boolean()
})
),
favourites: v.optional(v.array(v.string())),
history: v.optional(v.array(v.string()))
}).index('clerkid', ['clerkId'])
});
74 changes: 74 additions & 0 deletions src/convex/sync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
import { getAndUpdateUser, getUser, verifyJwtAndGetPayload } from './utils';

export const get = query({
args: {
jwt: v.string()
},
handler: async (ctx, args) => {
const payload = await verifyJwtAndGetPayload(args.jwt);
const userInfo = await getUser(ctx, payload);
if (!userInfo) {
return null;
}
return {
settings: userInfo.settings,
favourites: userInfo.favourites,
history: userInfo.history
};
}
});

export const update = mutation({
args: {
jwt: v.string(),
settings: v.optional(
v.object({
experimentalFeatures: v.boolean(),
open: v.string(),
theme: v.string(),
panic: v.object({
enabled: v.boolean(),
key: v.string(),
url: v.string(),
disableExperimentalMode: v.boolean()
}),
cloak: v.object({
mode: v.string(),
name: v.string(),
icon: v.string()
}),
history: v.boolean()
})
),
favourites: v.optional(v.array(v.string())),
history: v.optional(v.array(v.string()))
},
handler: async (ctx, args) => {
const payload = await verifyJwtAndGetPayload(args.jwt);
if (!payload.sub) {
throw new Error('Invalid JWT: missing subject');
}
const userInfo = await getAndUpdateUser(ctx, payload);
if (!userInfo?._id) {
throw new Error('Something went wrong');
}

if (args.favourites) {
await ctx.db.patch(userInfo._id, {
favourites: args.favourites
});
}
if (args.history) {
await ctx.db.patch(userInfo._id, {
history: args.history
});
}
if (args.settings) {
await ctx.db.patch(userInfo._id, {
settings: args.settings
});
}
}
});
18 changes: 18 additions & 0 deletions src/convex/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export interface JWTPayload {
email: string;
avatar: string;
lastname: string;
username: string;
verified: boolean;
firstname: string;
azp: string;
exp: number;
fva: number[];
iat: number;
iss: string;
nbf: number;
sid: string;
sub: string;
v: string;
fea: string;
}
69 changes: 69 additions & 0 deletions src/convex/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import * as jose from 'jose';
import type { MutationCtx, QueryCtx } from './_generated/server';
import type { JwtPayload } from 'jsonwebtoken';

// Shared helper function to verify JWT and return payload
export async function verifyJwtAndGetPayload(jwt: string) {
if (!process.env.CLERK_JWT_KEY) {
throw new Error('Missing CLERK_JWT_KEY environment variable');
}
const publicKey = await jose.importSPKI(process.env.CLERK_JWT_KEY, 'RS256');
if (jwt.length === 0) {
throw new Error('Missing JWT');
}
const { payload } = await jose.jwtVerify(jwt, publicKey, {});
if (!payload.sub) {
throw new Error('Invalid JWT');
}
return payload;
}

export async function getUser(ctx: QueryCtx, payload: JwtPayload) {
if (!payload.sub) {
throw new Error('Invalid JWT');
}
let user = await ctx.db
.query('users')
.withIndex('clerkid', (q) => q.eq('clerkId', payload.sub || ''))
.first();

if (!user) {
return null;
}

return user;
}

export async function getAndUpdateUser(ctx: MutationCtx, payload: JwtPayload) {
if (!payload.sub) {
throw new Error('Invalid JWT');
}
let user = await ctx.db
.query('users')
.withIndex('clerkid', (q) => q.eq('clerkId', payload.sub || ''))
.first();
if (user) {
await ctx.db.patch(user._id, {
avatar: payload.avatar,
email: payload.email,
firstName: payload.firstname,
lastName: payload.lastname,
username: payload.username,
verified: payload.verified,
clerkId: payload.sub
});
user = await ctx.db.get(user._id);
} else {
const userId = await ctx.db.insert('users', {
avatar: payload.avatar,
email: payload.email,
firstName: payload.firstname,
lastName: payload.firstname,
username: payload.username,
verified: payload.verified,
clerkId: payload.sub
});
user = await ctx.db.get(userId);
}
return user;
}
10 changes: 6 additions & 4 deletions src/lib/components/app-sidebar.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -253,12 +253,14 @@
{...props}
>
<Code />
EducationalTools/src
<div class="truncate">EducationalTools/src</div>
<div class="grow"></div>
<Badge>
<GitBranch />
{process.env.BRANCH_NAME}</Badge
<div
class="bg-muted text-muted-foreground pointer-events-none inline-flex h-5 items-center gap-1 truncate rounded border px-1.5 font-mono text-[10px] font-medium opacity-100 select-none"
>
<GitBranch class="size-2" />
{process.env.BRANCH_NAME}
</div>
</a>
{/snippet}
</Sidebar.MenuButton>
Expand Down
7 changes: 6 additions & 1 deletion src/lib/components/providers.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import { ClerkProvider, GoogleOneTap } from 'svelte-clerk/client';
import { ModeWatcher } from 'mode-watcher';
import { setupConvex } from 'convex-svelte';
import { dark } from '@clerk/themes';
import { mode } from 'mode-watcher';

// Props
let { children } = $props();
Expand All @@ -19,7 +21,10 @@
}
</script>

<ClerkProvider publishableKey={process.env.PUBLIC_CLERK_PUBLISHABLE_KEY || ''}>
<ClerkProvider
publishableKey={process.env.PUBLIC_CLERK_PUBLISHABLE_KEY || ''}
appearance={{ cssLayerName: 'clerk', ...(mode.current == 'dark' ? { baseTheme: dark } : {}) }}
>
<GoogleOneTap />
<ModeWatcher disableTransitions={false} defaultMode={'dark'} />
{@render children()}
Expand Down
Loading