Skip to content

Commit e661d83

Browse files
authored
Merge pull request #526 from EducationalTools/main
some changes
2 parents 9f78233 + 829fd21 commit e661d83

15 files changed

Lines changed: 498 additions & 54 deletions

File tree

.github/workflows/build.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,16 +6,15 @@ on:
66
jobs:
77
build:
88
runs-on: ubuntu-latest
9-
container:
10-
image: 'archlinux:latest'
119
permissions: write-all
1210

1311
steps:
1412
- name: Checkout code
1513
uses: actions/checkout@v2
1614

17-
- name: Install packages
18-
run: pacman -Sy pnpm nodejs npm chromium icu --noconfirm
15+
- uses: pnpm/action-setup@v4
16+
with:
17+
version: 9
1918

2019
- name: Install dependencies
2120
run: |

src/convex/_generated/api.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import type {
1414
FunctionReference,
1515
} from "convex/server";
1616
import type * as backups from "../backups.js";
17+
import type * as sync from "../sync.js";
18+
import type * as types from "../types.js";
19+
import type * as utils from "../utils.js";
1720

1821
/**
1922
* A utility for referencing Convex functions in your app's API.
@@ -25,6 +28,9 @@ import type * as backups from "../backups.js";
2528
*/
2629
declare const fullApi: ApiFromModules<{
2730
backups: typeof backups;
31+
sync: typeof sync;
32+
types: typeof types;
33+
utils: typeof utils;
2834
}>;
2935
export declare const api: FilterApi<
3036
typeof fullApi,

src/convex/backups.ts

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,22 @@
11
import { v } from 'convex/values';
22
import { mutation, query } from './_generated/server';
3-
import * as jose from 'jose';
4-
5-
// Shared helper function to verify JWT and return payload
6-
async function verifyJwtAndGetPayload(jwt: string) {
7-
if (!process.env.CLERK_JWT_KEY) {
8-
throw new Error('Missing CLERK_JWT_KEY environment variable');
9-
}
10-
const publicKey = await jose.importSPKI(process.env.CLERK_JWT_KEY, 'RS256');
11-
if (jwt.length === 0) {
12-
throw new Error('Missing JWT');
13-
}
14-
const { payload } = await jose.jwtVerify(jwt, publicKey, {});
15-
if (!payload.sub) {
16-
throw new Error('Invalid JWT');
17-
}
18-
return payload;
19-
}
3+
import { getAndUpdateUser, getUser, verifyJwtAndGetPayload } from './utils';
204

215
export const get = query({
226
args: {
237
jwt: v.string()
248
},
259
handler: async (ctx, args) => {
2610
const payload = await verifyJwtAndGetPayload(args.jwt);
11+
const userInfo = await getUser(ctx, payload);
12+
if (!userInfo) {
13+
return [];
14+
}
2715
const backups = await ctx.db
2816
.query('backup')
2917
.order('desc')
30-
.filter((q) => q.eq(q.field('user'), payload.sub))
31-
.take(100);
18+
.filter((q) => q.eq(q.field('user'), userInfo._id))
19+
.collect();
3220
return backups.map((backup) => ({
3321
name: backup.name,
3422
data: backup.data,
@@ -49,8 +37,12 @@ export const create = mutation({
4937
if (!payload.sub) {
5038
throw new Error('Invalid JWT: missing subject');
5139
}
40+
const userInfo = await getAndUpdateUser(ctx, payload);
41+
if (!userInfo?._id) {
42+
throw new Error('Something went wrong');
43+
}
5244
await ctx.db.insert('backup', {
53-
user: payload.sub,
45+
user: userInfo?._id,
5446
name: args.name,
5547
data: args.data
5648
});
@@ -65,9 +57,12 @@ export const remove = mutation({
6557
handler: async (ctx, args) => {
6658
const payload = await verifyJwtAndGetPayload(args.jwt);
6759
const backup = await ctx.db.get(args.id);
68-
if (backup?.user !== payload.sub) {
60+
const userInfo = await getAndUpdateUser(ctx, payload);
61+
62+
if (backup?.user !== userInfo?._id) {
6963
throw new Error('Unauthorized');
7064
}
65+
await getAndUpdateUser(ctx, payload);
7166
await ctx.db.delete(args.id);
7267
}
7368
});

src/convex/schema.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,41 @@ export default defineSchema({
55
comments: defineTable({
66
body: v.string(),
77
gmaeid: v.string(),
8-
user: v.string()
8+
user: v.id('users')
99
}),
1010
backup: defineTable({
1111
name: v.string(),
1212
data: v.string(),
13-
user: v.string()
14-
})
13+
user: v.id('users')
14+
}),
15+
users: defineTable({
16+
email: v.string(),
17+
firstName: v.optional(v.string()),
18+
lastName: v.optional(v.string()),
19+
avatar: v.optional(v.string()),
20+
username: v.string(),
21+
verified: v.boolean(),
22+
clerkId: v.string(),
23+
settings: v.optional(
24+
v.object({
25+
experimentalFeatures: v.boolean(),
26+
open: v.string(),
27+
theme: v.string(),
28+
panic: v.object({
29+
enabled: v.boolean(),
30+
key: v.string(),
31+
url: v.string(),
32+
disableExperimentalMode: v.boolean()
33+
}),
34+
cloak: v.object({
35+
mode: v.string(),
36+
name: v.string(),
37+
icon: v.string()
38+
}),
39+
history: v.boolean()
40+
})
41+
),
42+
favourites: v.optional(v.array(v.string())),
43+
history: v.optional(v.array(v.string()))
44+
}).index('clerkid', ['clerkId'])
1545
});

src/convex/sync.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { v } from 'convex/values';
2+
import { mutation, query } from './_generated/server';
3+
import { getAndUpdateUser, getUser, verifyJwtAndGetPayload } from './utils';
4+
5+
export const get = query({
6+
args: {
7+
jwt: v.string()
8+
},
9+
handler: async (ctx, args) => {
10+
const payload = await verifyJwtAndGetPayload(args.jwt);
11+
const userInfo = await getUser(ctx, payload);
12+
if (!userInfo) {
13+
return null;
14+
}
15+
return {
16+
settings: userInfo.settings,
17+
favourites: userInfo.favourites,
18+
history: userInfo.history
19+
};
20+
}
21+
});
22+
23+
export const update = mutation({
24+
args: {
25+
jwt: v.string(),
26+
settings: v.optional(
27+
v.object({
28+
experimentalFeatures: v.boolean(),
29+
open: v.string(),
30+
theme: v.string(),
31+
panic: v.object({
32+
enabled: v.boolean(),
33+
key: v.string(),
34+
url: v.string(),
35+
disableExperimentalMode: v.boolean()
36+
}),
37+
cloak: v.object({
38+
mode: v.string(),
39+
name: v.string(),
40+
icon: v.string()
41+
}),
42+
history: v.boolean()
43+
})
44+
),
45+
favourites: v.optional(v.array(v.string())),
46+
history: v.optional(v.array(v.string()))
47+
},
48+
handler: async (ctx, args) => {
49+
const payload = await verifyJwtAndGetPayload(args.jwt);
50+
if (!payload.sub) {
51+
throw new Error('Invalid JWT: missing subject');
52+
}
53+
const userInfo = await getAndUpdateUser(ctx, payload);
54+
if (!userInfo?._id) {
55+
throw new Error('Something went wrong');
56+
}
57+
58+
if (args.favourites) {
59+
await ctx.db.patch(userInfo._id, {
60+
favourites: args.favourites
61+
});
62+
}
63+
if (args.history) {
64+
await ctx.db.patch(userInfo._id, {
65+
history: args.history
66+
});
67+
}
68+
if (args.settings) {
69+
await ctx.db.patch(userInfo._id, {
70+
settings: args.settings
71+
});
72+
}
73+
}
74+
});

src/convex/types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
export interface JWTPayload {
2+
email: string;
3+
avatar: string;
4+
lastname: string;
5+
username: string;
6+
verified: boolean;
7+
firstname: string;
8+
azp: string;
9+
exp: number;
10+
fva: number[];
11+
iat: number;
12+
iss: string;
13+
nbf: number;
14+
sid: string;
15+
sub: string;
16+
v: string;
17+
fea: string;
18+
}

src/convex/utils.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import * as jose from 'jose';
2+
import type { MutationCtx, QueryCtx } from './_generated/server';
3+
import type { JwtPayload } from 'jsonwebtoken';
4+
5+
// Shared helper function to verify JWT and return payload
6+
export async function verifyJwtAndGetPayload(jwt: string) {
7+
if (!process.env.CLERK_JWT_KEY) {
8+
throw new Error('Missing CLERK_JWT_KEY environment variable');
9+
}
10+
const publicKey = await jose.importSPKI(process.env.CLERK_JWT_KEY, 'RS256');
11+
if (jwt.length === 0) {
12+
throw new Error('Missing JWT');
13+
}
14+
const { payload } = await jose.jwtVerify(jwt, publicKey, {});
15+
if (!payload.sub) {
16+
throw new Error('Invalid JWT');
17+
}
18+
return payload;
19+
}
20+
21+
export async function getUser(ctx: QueryCtx, payload: JwtPayload) {
22+
if (!payload.sub) {
23+
throw new Error('Invalid JWT');
24+
}
25+
let user = await ctx.db
26+
.query('users')
27+
.withIndex('clerkid', (q) => q.eq('clerkId', payload.sub || ''))
28+
.first();
29+
30+
if (!user) {
31+
return null;
32+
}
33+
34+
return user;
35+
}
36+
37+
export async function getAndUpdateUser(ctx: MutationCtx, payload: JwtPayload) {
38+
if (!payload.sub) {
39+
throw new Error('Invalid JWT');
40+
}
41+
let user = await ctx.db
42+
.query('users')
43+
.withIndex('clerkid', (q) => q.eq('clerkId', payload.sub || ''))
44+
.first();
45+
if (user) {
46+
await ctx.db.patch(user._id, {
47+
avatar: payload.avatar,
48+
email: payload.email,
49+
firstName: payload.firstname,
50+
lastName: payload.lastname,
51+
username: payload.username,
52+
verified: payload.verified,
53+
clerkId: payload.sub
54+
});
55+
user = await ctx.db.get(user._id);
56+
} else {
57+
const userId = await ctx.db.insert('users', {
58+
avatar: payload.avatar,
59+
email: payload.email,
60+
firstName: payload.firstname,
61+
lastName: payload.firstname,
62+
username: payload.username,
63+
verified: payload.verified,
64+
clerkId: payload.sub
65+
});
66+
user = await ctx.db.get(userId);
67+
}
68+
return user;
69+
}

src/lib/components/app-sidebar.svelte

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,12 +253,14 @@
253253
{...props}
254254
>
255255
<Code />
256-
EducationalTools/src
256+
<div class="truncate">EducationalTools/src</div>
257257
<div class="grow"></div>
258-
<Badge>
259-
<GitBranch />
260-
{process.env.BRANCH_NAME}</Badge
258+
<div
259+
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"
261260
>
261+
<GitBranch class="size-2" />
262+
{process.env.BRANCH_NAME}
263+
</div>
262264
</a>
263265
{/snippet}
264266
</Sidebar.MenuButton>

src/lib/components/providers.svelte

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
import { ClerkProvider, GoogleOneTap } from 'svelte-clerk/client';
33
import { ModeWatcher } from 'mode-watcher';
44
import { setupConvex } from 'convex-svelte';
5+
import { dark } from '@clerk/themes';
6+
import { mode } from 'mode-watcher';
57
68
// Props
79
let { children } = $props();
@@ -19,7 +21,10 @@
1921
}
2022
</script>
2123

22-
<ClerkProvider publishableKey={process.env.PUBLIC_CLERK_PUBLISHABLE_KEY || ''}>
24+
<ClerkProvider
25+
publishableKey={process.env.PUBLIC_CLERK_PUBLISHABLE_KEY || ''}
26+
appearance={{ cssLayerName: 'clerk', ...(mode.current == 'dark' ? { baseTheme: dark } : {}) }}
27+
>
2328
<GoogleOneTap />
2429
<ModeWatcher disableTransitions={false} defaultMode={'dark'} />
2530
{@render children()}

0 commit comments

Comments
 (0)