diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 1d334744..0add09a9 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -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: |
diff --git a/src/convex/_generated/api.d.ts b/src/convex/_generated/api.d.ts
index bf758f5d..19187b5f 100644
--- a/src/convex/_generated/api.d.ts
+++ b/src/convex/_generated/api.d.ts
@@ -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.
@@ -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,
diff --git a/src/convex/backups.ts b/src/convex/backups.ts
index 63280174..de1c060e 100644
--- a/src/convex/backups.ts
+++ b/src/convex/backups.ts
@@ -1,22 +1,6 @@
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: {
@@ -24,11 +8,15 @@ export const get = query({
},
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,
@@ -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
});
@@ -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);
}
});
diff --git a/src/convex/schema.ts b/src/convex/schema.ts
index 5b2393db..b689eac1 100644
--- a/src/convex/schema.ts
+++ b/src/convex/schema.ts
@@ -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'])
});
diff --git a/src/convex/sync.ts b/src/convex/sync.ts
new file mode 100644
index 00000000..52aa9cba
--- /dev/null
+++ b/src/convex/sync.ts
@@ -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
+ });
+ }
+ }
+});
diff --git a/src/convex/types.ts b/src/convex/types.ts
new file mode 100644
index 00000000..b6a5f8e1
--- /dev/null
+++ b/src/convex/types.ts
@@ -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;
+}
diff --git a/src/convex/utils.ts b/src/convex/utils.ts
new file mode 100644
index 00000000..77170ae2
--- /dev/null
+++ b/src/convex/utils.ts
@@ -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;
+}
diff --git a/src/lib/components/app-sidebar.svelte b/src/lib/components/app-sidebar.svelte
index e0d8b34e..c0fc34ed 100644
--- a/src/lib/components/app-sidebar.svelte
+++ b/src/lib/components/app-sidebar.svelte
@@ -253,12 +253,14 @@
{...props}
>
- EducationalTools/src
+