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
Inglan Sep 19, 2025
477aab4
move to another file
Inglan Sep 19, 2025
6a3b907
add jwt payload type
Inglan Sep 19, 2025
85ccbf4
add clerkid to schema
Inglan Sep 19, 2025
69e555b
add indexing by clerk id
Inglan Sep 19, 2025
6a73f04
add code to get and update user
Inglan Sep 19, 2025
9a7663d
make user an id
Inglan Sep 19, 2025
31452da
change some stuff
Inglan Sep 19, 2025
af0e474
use userid
Inglan Sep 19, 2025
71337a5
add get user info function
Inglan Sep 19, 2025
a2996e9
use new function and stuff
Inglan Sep 19, 2025
372da0e
add optional fields
Inglan Sep 19, 2025
caf4512
change some settings for clerk
Inglan Sep 20, 2025
7c5e643
create account route
Inglan Sep 20, 2025
7989196
remove dialog
Inglan Sep 20, 2025
a455402
add active state for profile button
Inglan Sep 20, 2025
a1b827f
add sync settings store
Inglan Sep 20, 2025
78a0f32
add sync item
Inglan Sep 20, 2025
9534846
grey out icon when disabled
Inglan Sep 20, 2025
5909522
add syncing state
Inglan Sep 20, 2025
1302f57
add sync state to sidebar
Inglan Sep 20, 2025
dd67dc4
add animation when syncing
Inglan Sep 20, 2025
36ac954
add checkboxes
Inglan Sep 20, 2025
9a20581
add more sync settings
Inglan Sep 20, 2025
2ab1a0c
add more sync settings
Inglan Sep 20, 2025
a36b95b
bind settings to local storage
Inglan Sep 20, 2025
be88fa9
transition no matter the state
Inglan Sep 20, 2025
abf8599
disabled settings when sync is disabled
Inglan Sep 20, 2025
52f81b4
fix non existent user handling
Inglan Sep 20, 2025
7447775
fix verification
Inglan Sep 20, 2025
231edf8
remove unused analytics setting
Inglan Sep 20, 2025
54adb8c
add schema for settings
Inglan Sep 20, 2025
449944a
add favourites and history
Inglan Sep 20, 2025
969fb22
make sync things optional
Inglan Sep 20, 2025
289447b
add sync get query
Inglan Sep 20, 2025
ec2a7bc
add mutation for sync update
Inglan Sep 20, 2025
b01637f
change branch name thing
Inglan Sep 20, 2025
7268603
add functions for sync
Inglan Sep 21, 2025
25b7685
make functions async and change syncstate
Inglan Sep 21, 2025
f0b393c
change some stuff
Inglan Sep 21, 2025
ec742fa
move everything to one sync function
Inglan Sep 21, 2025
21ddbf6
fix issue with svelte
Inglan Sep 21, 2025
135a72e
fix issue with users that still have analytics key in local stoarge
Inglan Sep 21, 2025
8235588
add slightly better error handling
Inglan Sep 21, 2025
c5c6a88
add code to sync settings
Inglan Sep 21, 2025
450215d
add handler for settings change
Inglan Sep 21, 2025
bd34242
move analytics and sync to settings change function
Inglan Sep 21, 2025
74f308e
pass convex client down
Inglan Sep 21, 2025
8a84cde
disable sync for now
Inglan Sep 23, 2025
da14aa9
fix issue
Inglan Sep 23, 2025
545b726
Merge pull request #524 from EducationalTools/510-settings-sync-with-…
Inglan Sep 23, 2025
829fd21
use the same workflow for build
Inglan 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