Update repo
This commit is contained in:
38
packages/backend/convex/_generated/api.d.ts
vendored
38
packages/backend/convex/_generated/api.d.ts
vendored
@@ -8,34 +8,54 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import type * as auth from "../auth.js";
|
||||
import type * as custom_auth_index from "../custom/auth/index.js";
|
||||
import type * as custom_auth_providers_password from "../custom/auth/providers/password.js";
|
||||
import type * as custom_auth_providers_usesend from "../custom/auth/providers/usesend.js";
|
||||
import type * as http from "../http.js";
|
||||
import type * as questions from "../questions.js";
|
||||
import type * as utils from "../utils.js";
|
||||
|
||||
import type {
|
||||
ApiFromModules,
|
||||
FilterApi,
|
||||
FunctionReference,
|
||||
} from "convex/server";
|
||||
|
||||
import type * as notes from "../notes.js";
|
||||
import type * as openai from "../openai.js";
|
||||
import type * as utils from "../utils.js";
|
||||
declare const fullApi: ApiFromModules<{
|
||||
auth: typeof auth;
|
||||
"custom/auth/index": typeof custom_auth_index;
|
||||
"custom/auth/providers/password": typeof custom_auth_providers_password;
|
||||
"custom/auth/providers/usesend": typeof custom_auth_providers_usesend;
|
||||
http: typeof http;
|
||||
questions: typeof questions;
|
||||
utils: typeof utils;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
* A utility for referencing Convex functions in your app's public API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = api.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
declare const fullApi: ApiFromModules<{
|
||||
notes: typeof notes;
|
||||
openai: typeof openai;
|
||||
utils: typeof utils;
|
||||
}>;
|
||||
export declare const api: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "public">
|
||||
>;
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's internal API.
|
||||
*
|
||||
* Usage:
|
||||
* ```js
|
||||
* const myFunctionReference = internal.myModule.myFunction;
|
||||
* ```
|
||||
*/
|
||||
export declare const internal: FilterApi<
|
||||
typeof fullApi,
|
||||
FunctionReference<any, "internal">
|
||||
>;
|
||||
|
||||
export declare const components: {};
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @module
|
||||
*/
|
||||
|
||||
import { anyApi } from "convex/server";
|
||||
import { anyApi, componentsGeneric } from "convex/server";
|
||||
|
||||
/**
|
||||
* A utility for referencing Convex functions in your app's API.
|
||||
@@ -20,3 +20,4 @@ import { anyApi } from "convex/server";
|
||||
*/
|
||||
export const api = anyApi;
|
||||
export const internal = anyApi;
|
||||
export const components = componentsGeneric();
|
||||
|
||||
@@ -11,11 +11,10 @@
|
||||
import type {
|
||||
DataModelFromSchemaDefinition,
|
||||
DocumentByName,
|
||||
SystemTableNames,
|
||||
TableNamesInDataModel,
|
||||
SystemTableNames,
|
||||
} from "convex/server";
|
||||
import type { GenericId } from "convex/values";
|
||||
|
||||
import schema from "../schema.js";
|
||||
|
||||
/**
|
||||
@@ -39,7 +38,7 @@ export type Doc<TableName extends TableNames> = DocumentByName<
|
||||
* Convex documents are uniquely identified by their `Id`, which is accessible
|
||||
* on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids).
|
||||
*
|
||||
* Documents can be loaded using `db.get(id)` in query and mutation functions.
|
||||
* Documents can be loaded using `db.get(tableName, id)` in query and mutation functions.
|
||||
*
|
||||
* IDs are just strings at runtime, but this type can be used to distinguish them from other
|
||||
* strings when type checking.
|
||||
|
||||
20
packages/backend/convex/_generated/server.d.ts
vendored
20
packages/backend/convex/_generated/server.d.ts
vendored
@@ -10,16 +10,15 @@
|
||||
|
||||
import {
|
||||
ActionBuilder,
|
||||
GenericActionCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
HttpActionBuilder,
|
||||
MutationBuilder,
|
||||
QueryBuilder,
|
||||
GenericActionCtx,
|
||||
GenericMutationCtx,
|
||||
GenericQueryCtx,
|
||||
GenericDatabaseReader,
|
||||
GenericDatabaseWriter,
|
||||
} from "convex/server";
|
||||
|
||||
import type { DataModel } from "./dataModel.js";
|
||||
|
||||
/**
|
||||
@@ -86,11 +85,12 @@ export declare const internalAction: ActionBuilder<DataModel, "internal">;
|
||||
/**
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* This function will be used to respond to HTTP requests received by a Convex
|
||||
* deployment if the requests matches the path and method where this action
|
||||
* is routed. Be sure to route your action in `convex/http.js`.
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument.
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export declare const httpAction: HttpActionBuilder;
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
import {
|
||||
actionGeneric,
|
||||
httpActionGeneric,
|
||||
queryGeneric,
|
||||
mutationGeneric,
|
||||
internalActionGeneric,
|
||||
internalMutationGeneric,
|
||||
internalQueryGeneric,
|
||||
mutationGeneric,
|
||||
queryGeneric,
|
||||
} from "convex/server";
|
||||
|
||||
/**
|
||||
@@ -80,10 +80,14 @@ export const action = actionGeneric;
|
||||
export const internalAction = internalActionGeneric;
|
||||
|
||||
/**
|
||||
* Define a Convex HTTP action.
|
||||
* Define an HTTP action.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object
|
||||
* as its second.
|
||||
* @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`.
|
||||
* The wrapped function will be used to respond to HTTP requests received
|
||||
* by a Convex deployment if the requests matches the path and method where
|
||||
* this action is routed. Be sure to route your httpAction in `convex/http.js`.
|
||||
*
|
||||
* @param func - The function. It receives an {@link ActionCtx} as its first argument
|
||||
* and a Fetch API `Request` object as its second.
|
||||
* @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
|
||||
*/
|
||||
export const httpAction = httpActionGeneric;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export default {
|
||||
providers: [
|
||||
{
|
||||
domain: process.env.CLERK_ISSUER_URL,
|
||||
domain: process.env.CONVEX_SITE_URL,
|
||||
applicationID: "convex",
|
||||
},
|
||||
],
|
||||
121
packages/backend/convex/auth.ts
Normal file
121
packages/backend/convex/auth.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import { ConvexError, v } from 'convex/values';
|
||||
import {
|
||||
convexAuth,
|
||||
getAuthUserId,
|
||||
retrieveAccount,
|
||||
modifyAccountCredentials,
|
||||
} from '@convex-dev/auth/server';
|
||||
import { api } from './_generated/api';
|
||||
import type { Doc, Id } from './_generated/dataModel';
|
||||
import {
|
||||
action,
|
||||
mutation,
|
||||
query,
|
||||
type MutationCtx,
|
||||
type QueryCtx,
|
||||
} from './_generated/server';
|
||||
import Authentik from '@auth/core/providers/authentik';
|
||||
import { Password, validatePassword } from './custom/auth';
|
||||
|
||||
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
|
||||
providers: [
|
||||
Authentik({ allowDangerousEmailAccountLinking: true }),
|
||||
Password,
|
||||
],
|
||||
});
|
||||
|
||||
const getUserById = async (
|
||||
ctx: QueryCtx,
|
||||
userId: Id<'users'>
|
||||
): Promise<Doc<'users'>> => {
|
||||
const user = await ctx.db.get(userId);
|
||||
if (!user) throw new ConvexError('User not found.');
|
||||
return user;
|
||||
};
|
||||
const isSignedIn = async (ctx: QueryCtx): Promise<Doc<'users'> | null> => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) return null;
|
||||
const user = await ctx.db.get(userId);
|
||||
if (!user) return null;
|
||||
return user;
|
||||
};
|
||||
|
||||
export const getUser = query({
|
||||
args: { userId: v.optional(v.id('users')) },
|
||||
handler: async (ctx, args) => {
|
||||
const user = await isSignedIn(ctx);
|
||||
const userId = args.userId ?? user?._id;
|
||||
if (!userId) throw new ConvexError('Not authenticated or no ID provided.');
|
||||
return getUserById(ctx, userId);
|
||||
},
|
||||
});
|
||||
|
||||
export const getAllUsers = query(async (ctx) => {
|
||||
const users = await ctx.db.query('users').collect();
|
||||
return users ?? null;
|
||||
});
|
||||
|
||||
export const getAllUserIds = query(async (ctx) => {
|
||||
const users = await ctx.db.query('users').collect();
|
||||
return users.map((u) => u._id);
|
||||
});
|
||||
|
||||
export const updateUser = mutation({
|
||||
args: {
|
||||
name: v.optional(v.string()),
|
||||
email: v.optional(v.string()),
|
||||
image: v.optional(v.id('_storage')),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new ConvexError('Not authenticated.');
|
||||
const user = await ctx.db.get(userId);
|
||||
if (!user) throw new ConvexError('User not found.');
|
||||
const patch: Partial<{
|
||||
name: string;
|
||||
email: string;
|
||||
image: Id<'_storage'>;
|
||||
}> = {};
|
||||
if (args.name !== undefined) patch.name = args.name;
|
||||
if (args.email !== undefined) patch.email = args.email;
|
||||
if (args.image !== undefined) {
|
||||
const oldImage = user.image as Id<'_storage'> | undefined;
|
||||
patch.image = args.image;
|
||||
if (oldImage && oldImage !== args.image) {
|
||||
await ctx.storage.delete(oldImage);
|
||||
}
|
||||
}
|
||||
if (Object.keys(patch).length > 0) {
|
||||
await ctx.db.patch(userId, patch);
|
||||
}
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
|
||||
export const updateUserPassword = action({
|
||||
args: {
|
||||
currentPassword: v.string(),
|
||||
newPassword: v.string(),
|
||||
},
|
||||
handler: async (ctx, { currentPassword, newPassword }) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new ConvexError('Not authenticated.');
|
||||
const user = await ctx.runQuery(api.auth.getUser, { userId });
|
||||
if (!user?.email) throw new ConvexError('User not found.');
|
||||
const verified = await retrieveAccount(ctx, {
|
||||
provider: 'password',
|
||||
account: { id: user.email, secret: currentPassword },
|
||||
});
|
||||
if (!verified) throw new ConvexError('Current password is incorrect.');
|
||||
|
||||
if (!validatePassword(newPassword))
|
||||
throw new ConvexError('Invalid password.');
|
||||
|
||||
await modifyAccountCredentials(ctx, {
|
||||
provider: 'password',
|
||||
account: { id: user.email, secret: newPassword },
|
||||
});
|
||||
|
||||
return { success: true };
|
||||
},
|
||||
});
|
||||
2
packages/backend/convex/custom/auth/index.ts
Normal file
2
packages/backend/convex/custom/auth/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { Password, validatePassword } from './providers/password';
|
||||
export { UseSendOTP, UseSendOTPPasswordReset } from './providers/usesend';
|
||||
33
packages/backend/convex/custom/auth/providers/password.ts
Normal file
33
packages/backend/convex/custom/auth/providers/password.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { Password as DefaultPassword } from '@convex-dev/auth/providers/Password';
|
||||
import { DataModel } from '../../../_generated/dataModel';
|
||||
import { UseSendOTP, UseSendOTPPasswordReset } from '..';
|
||||
import { ConvexError } from 'convex/values';
|
||||
|
||||
export const Password = DefaultPassword<DataModel>({
|
||||
profile(params, ctx) {
|
||||
return {
|
||||
email: params.email as string,
|
||||
name: params.name as string,
|
||||
};
|
||||
},
|
||||
validatePasswordRequirements: (password: string) => {
|
||||
if (!validatePassword(password)) {
|
||||
throw new ConvexError('Invalid password.');
|
||||
}
|
||||
},
|
||||
reset: UseSendOTPPasswordReset,
|
||||
verify: UseSendOTP,
|
||||
});
|
||||
|
||||
export const validatePassword = (password: string): boolean => {
|
||||
if (
|
||||
password.length < 8 ||
|
||||
password.length > 100 ||
|
||||
!/\d/.test(password) ||
|
||||
!/[a-z]/.test(password) ||
|
||||
!/[A-Z]/.test(password)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
90
packages/backend/convex/custom/auth/providers/usesend.ts
Normal file
90
packages/backend/convex/custom/auth/providers/usesend.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import type { EmailConfig, EmailUserConfig } from '@auth/core/providers/email';
|
||||
import { alphabet } from 'oslo/crypto';
|
||||
import { generateRandomString, RandomReader } from '@oslojs/crypto/random';
|
||||
import { UseSend } from 'usesend-js';
|
||||
|
||||
export default function UseSendProvider(config: EmailUserConfig): EmailConfig {
|
||||
return {
|
||||
id: 'usesend',
|
||||
type: 'email',
|
||||
name: 'UseSend',
|
||||
from: 'TechTracker <admin@techtracker.gbrown.org>',
|
||||
maxAge: 24 * 60 * 60, // 24 hours
|
||||
|
||||
async generateVerificationToken() {
|
||||
const random: RandomReader = {
|
||||
read(bytes) {
|
||||
crypto.getRandomValues(bytes);
|
||||
},
|
||||
};
|
||||
return generateRandomString(random, alphabet('0-9'), 6);
|
||||
},
|
||||
|
||||
async sendVerificationRequest(params) {
|
||||
const { identifier: to, provider, url, theme, token } = params;
|
||||
//const { host } = new URL(url);
|
||||
const host = 'TechTracker';
|
||||
|
||||
const useSend = new UseSend(
|
||||
process.env.AUTH_USESEND_API_KEY!,
|
||||
'https://usesend.gbrown.org',
|
||||
);
|
||||
|
||||
// For password reset, we want to send the code, not the magic link
|
||||
const isPasswordReset =
|
||||
url.includes('reset') || provider.id?.includes('reset');
|
||||
|
||||
const result = await useSend.emails.send({
|
||||
from: provider.from!,
|
||||
to: [to],
|
||||
subject: isPasswordReset
|
||||
? `Reset your password - ${host}`
|
||||
: `Sign in to ${host}`,
|
||||
text: isPasswordReset
|
||||
? `Your password reset code is ${token}`
|
||||
: `Your sign in code is ${token}`,
|
||||
html: isPasswordReset
|
||||
? `
|
||||
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
|
||||
<h2>Password Reset Request</h2>
|
||||
<p>You requested a password reset. Your reset code is:</p>
|
||||
<div style="font-size: 32px; font-weight: bold; text-align: center; padding: 20px; background: #f5f5f5; margin: 20px 0; border-radius: 8px;">
|
||||
${token}
|
||||
</div>
|
||||
<p>This code expires in 1 hour.</p>
|
||||
<p>If you didn't request this, please ignore this email.</p>
|
||||
</div>
|
||||
`
|
||||
: `
|
||||
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
|
||||
<h2>Your Sign In Code</h2>
|
||||
<p>Your verification code is:</p>
|
||||
<div style="font-size: 32px; font-weight: bold; text-align: center; padding: 20px; background: #f5f5f5; margin: 20px 0; border-radius: 8px;">
|
||||
${token}
|
||||
</div>
|
||||
<p>This code expires in 24 hours.</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw new Error('UseSend error: ' + JSON.stringify(result.error));
|
||||
}
|
||||
},
|
||||
|
||||
options: config,
|
||||
};
|
||||
}
|
||||
|
||||
// Create specific instances for password reset and email verification
|
||||
export const UseSendOTPPasswordReset = UseSendProvider({
|
||||
id: 'usesend-otp-password-reset',
|
||||
apiKey: process.env.AUTH_USESEND_API_KEY,
|
||||
maxAge: 60 * 60, // 1 hour
|
||||
});
|
||||
|
||||
export const UseSendOTP = UseSendProvider({
|
||||
id: 'usesend-otp',
|
||||
apiKey: process.env.AUTH_USESEND_API_KEY,
|
||||
maxAge: 60 * 20, // 20 minutes
|
||||
});
|
||||
8
packages/backend/convex/http.ts
Normal file
8
packages/backend/convex/http.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { httpRouter } from 'convex/server';
|
||||
import { auth } from './auth';
|
||||
|
||||
const http = httpRouter();
|
||||
|
||||
auth.addHttpRoutes(http);
|
||||
|
||||
export default http;
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Auth } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
import { internal } from "../convex/_generated/api";
|
||||
import { mutation, query } from "./_generated/server";
|
||||
|
||||
export const getUserId = async (ctx: { auth: Auth }) => {
|
||||
return (await ctx.auth.getUserIdentity())?.subject;
|
||||
};
|
||||
|
||||
// Get all notes for a specific user
|
||||
export const getNotes = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const userId = await getUserId(ctx);
|
||||
if (!userId) return null;
|
||||
|
||||
const notes = await ctx.db
|
||||
.query("notes")
|
||||
.filter((q) => q.eq(q.field("userId"), userId))
|
||||
.collect();
|
||||
|
||||
return notes;
|
||||
},
|
||||
});
|
||||
|
||||
// Get note for a specific note
|
||||
export const getNote = query({
|
||||
args: {
|
||||
id: v.optional(v.id("notes")),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const { id } = args;
|
||||
if (!id) return null;
|
||||
const note = await ctx.db.get(id);
|
||||
return note;
|
||||
},
|
||||
});
|
||||
|
||||
// Create a new note for a user
|
||||
export const createNote = mutation({
|
||||
args: {
|
||||
title: v.string(),
|
||||
content: v.string(),
|
||||
isSummary: v.boolean(),
|
||||
},
|
||||
handler: async (ctx, { title, content, isSummary }) => {
|
||||
const userId = await getUserId(ctx);
|
||||
if (!userId) throw new Error("User not found");
|
||||
const noteId = await ctx.db.insert("notes", { userId, title, content });
|
||||
|
||||
if (isSummary) {
|
||||
await ctx.scheduler.runAfter(0, internal.openai.summary, {
|
||||
id: noteId,
|
||||
title,
|
||||
content,
|
||||
});
|
||||
}
|
||||
|
||||
return noteId;
|
||||
},
|
||||
});
|
||||
|
||||
export const deleteNote = mutation({
|
||||
args: {
|
||||
noteId: v.id("notes"),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
await ctx.db.delete(args.noteId);
|
||||
},
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
import { v } from "convex/values";
|
||||
import OpenAI from "openai";
|
||||
|
||||
import { internal } from "./_generated/api";
|
||||
import { internalAction, internalMutation, query } from "./_generated/server";
|
||||
import { missingEnvVariableUrl } from "./utils";
|
||||
|
||||
export const openaiKeySet = query({
|
||||
args: {},
|
||||
handler: async () => {
|
||||
return !!process.env.OPENAI_API_KEY;
|
||||
},
|
||||
});
|
||||
|
||||
export const summary = internalAction({
|
||||
args: {
|
||||
id: v.id("notes"),
|
||||
title: v.string(),
|
||||
content: v.string(),
|
||||
},
|
||||
handler: async (ctx, { id, title, content }) => {
|
||||
const prompt = `Take in the following note and return a summary: Title: ${title}, Note content: ${content}`;
|
||||
|
||||
const apiKey = process.env.OPENAI_API_KEY;
|
||||
if (!apiKey) {
|
||||
const error = missingEnvVariableUrl(
|
||||
"OPENAI_API_KEY",
|
||||
"https://platform.openai.com/account/api-keys",
|
||||
);
|
||||
console.error(error);
|
||||
await ctx.runMutation(internal.openai.saveSummary, {
|
||||
id: id,
|
||||
summary: error,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const openai = new OpenAI({ apiKey });
|
||||
const output = await openai.chat.completions.create({
|
||||
messages: [
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
"You are a helpful assistant designed to output JSON in this format: {summary: string}",
|
||||
},
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
model: "gpt-4-1106-preview",
|
||||
response_format: { type: "json_object" },
|
||||
});
|
||||
|
||||
// Pull the message content out of the response
|
||||
const messageContent = output.choices[0]?.message.content;
|
||||
|
||||
console.log({ messageContent });
|
||||
|
||||
const parsedOutput = JSON.parse(messageContent!);
|
||||
console.log({ parsedOutput });
|
||||
|
||||
await ctx.runMutation(internal.openai.saveSummary, {
|
||||
id: id,
|
||||
summary: parsedOutput.summary,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const saveSummary = internalMutation({
|
||||
args: {
|
||||
id: v.id("notes"),
|
||||
summary: v.string(),
|
||||
},
|
||||
handler: async (ctx, { id, summary }) => {
|
||||
await ctx.db.patch(id, {
|
||||
summary: summary,
|
||||
});
|
||||
},
|
||||
});
|
||||
116
packages/backend/convex/questions.ts
Normal file
116
packages/backend/convex/questions.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { v } from "convex/values";
|
||||
import { query, mutation, action } from "./_generated/server";
|
||||
import { getAuthUserId } from "@convex-dev/auth/server";
|
||||
import { api } from "./_generated/api";
|
||||
import OpenAI from "openai";
|
||||
|
||||
const openai = new OpenAI({
|
||||
baseURL: process.env.OPENAI_BASE_URL,
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
export const getAllQuestions = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
return await ctx.db.query("questions").collect();
|
||||
},
|
||||
});
|
||||
|
||||
export const getQuestionsByTopic = query({
|
||||
args: { topic: v.string() },
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
return await ctx.db
|
||||
.query("questions")
|
||||
.withIndex("by_topic", (q) => q.eq("topic", args.topic))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
export const addQuestion = mutation({
|
||||
args: {
|
||||
question: v.string(),
|
||||
options: v.array(v.string()),
|
||||
correctAnswer: v.number(),
|
||||
topic: v.string(),
|
||||
difficulty: v.optional(v.string()),
|
||||
explanation: v.optional(v.string()),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
return await ctx.db.insert("questions", {
|
||||
question: args.question,
|
||||
options: args.options,
|
||||
correctAnswer: args.correctAnswer,
|
||||
topic: args.topic,
|
||||
difficulty: args.difficulty,
|
||||
explanation: args.explanation,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const generateQuestions = action({
|
||||
args: {
|
||||
topic: v.string(),
|
||||
count: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
const userId = await getAuthUserId(ctx);
|
||||
if (!userId) throw new Error("Not authenticated");
|
||||
const prompt = `Generate ${args.count} multiple choice questions for the CompTIA Network+ exam on the topic: "${args.topic}".
|
||||
|
||||
Return ONLY a valid JSON array with this exact structure:
|
||||
[
|
||||
{
|
||||
"question": "Question text here?",
|
||||
"options": ["Option A", "Option B", "Option C", "Option D"],
|
||||
"correctAnswer": 0,
|
||||
"topic": "${args.topic}",
|
||||
"difficulty": "medium",
|
||||
"explanation": "Brief explanation of the correct answer"
|
||||
}
|
||||
]
|
||||
|
||||
Rules:
|
||||
- Each question must have exactly 4 options
|
||||
- correctAnswer is the index (0-3) of the correct option
|
||||
- Make questions realistic for Network+ certification
|
||||
- Include technical details and scenarios
|
||||
- Return ONLY the JSON array, no other text`;
|
||||
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [{ role: "user", content: prompt }],
|
||||
temperature: 0.8,
|
||||
});
|
||||
|
||||
const content = response.choices[0].message.content;
|
||||
if (!content) throw new Error("No response from AI");
|
||||
|
||||
let questions;
|
||||
try {
|
||||
questions = JSON.parse(content);
|
||||
} catch (e) {
|
||||
throw new Error("Failed to parse AI response as JSON");
|
||||
}
|
||||
|
||||
const questionIds = [];
|
||||
for (const q of questions) {
|
||||
const id: string = await ctx.runMutation(api.questions.addQuestion, {
|
||||
question: q.question,
|
||||
options: q.options,
|
||||
correctAnswer: q.correctAnswer,
|
||||
topic: q.topic,
|
||||
difficulty: q.difficulty,
|
||||
explanation: q.explanation,
|
||||
});
|
||||
questionIds.push(id);
|
||||
}
|
||||
|
||||
return questionIds;
|
||||
},
|
||||
});
|
||||
@@ -1,11 +1,40 @@
|
||||
import { defineSchema, defineTable } from "convex/server";
|
||||
import { v } from "convex/values";
|
||||
import { v, VId } from "convex/values";
|
||||
import { authTables } from '@convex-dev/auth/server';
|
||||
|
||||
const applicationTables = {
|
||||
questions: defineTable({
|
||||
question: v.string(),
|
||||
options: v.array(v.string()),
|
||||
correctAnswer: v.number(),
|
||||
topic: v.string(),
|
||||
difficulty: v.optional(v.string()),
|
||||
explanation: v.optional(v.string()),
|
||||
})
|
||||
.index('by_topic', ['topic']),
|
||||
|
||||
userProgress: defineTable({
|
||||
userId: v.id('users'),
|
||||
questionId: v.id('questions'),
|
||||
correctCount: v.number(),
|
||||
incorrectCount: v.number(),
|
||||
lastAttempted: v.number(),
|
||||
})
|
||||
.index('by_user', ['userId'])
|
||||
.index('by_user_and_question', ['userId', 'questionId']),
|
||||
|
||||
quizSessions: defineTable({
|
||||
userId: v.id('users'),
|
||||
activeQuestions: v.array(v.id('questions')),
|
||||
missedQuestions: v.array(v.id('questions')),
|
||||
completedQuestions: v.array(v.id('questions')),
|
||||
currentQuestionIndex: v.number(),
|
||||
isActive: v.boolean(),
|
||||
})
|
||||
.index('by_user_and_active', ['userId', 'isActive']),
|
||||
};
|
||||
|
||||
export default defineSchema({
|
||||
notes: defineTable({
|
||||
userId: v.string(),
|
||||
title: v.string(),
|
||||
content: v.string(),
|
||||
summary: v.optional(v.string()),
|
||||
}),
|
||||
...authTables,
|
||||
...applicationTables,
|
||||
});
|
||||
|
||||
@@ -8,9 +8,9 @@
|
||||
"license": "MIT",
|
||||
"exports": {},
|
||||
"scripts": {
|
||||
"dev": "convex dev",
|
||||
"dev:tunnel": "convex dev",
|
||||
"setup": "convex dev --until-success",
|
||||
"dev": "dotenv -e ../../.env -- convex dev",
|
||||
"dev:tunnel": "dotenv -e ../../.env -- convex dev",
|
||||
"setup": "dotenv -e ../../.env -- convex dev --until-success",
|
||||
"clean": "git clean -xdf .cache .turbo dist node_modules",
|
||||
"format": "prettier --check . --ignore-path ../../.gitignore",
|
||||
"lint": "eslint --flag unstable_native_nodejs_ts_config",
|
||||
@@ -21,6 +21,7 @@
|
||||
"@react-email/components": "0.5.4",
|
||||
"@react-email/render": "^1.4.0",
|
||||
"convex": "catalog:convex",
|
||||
"openai": "catalog:",
|
||||
"react": "catalog:react19",
|
||||
"react-dom": "catalog:react19",
|
||||
"usesend-js": "^1.5.6",
|
||||
|
||||
Reference in New Issue
Block a user