Made great progress on monorepo & auth for next. Very happy with work!

This commit is contained in:
2026-01-12 11:55:15 -06:00
parent 72f11f0b02
commit 321fecb5e1
58 changed files with 1266 additions and 222 deletions

File diff suppressed because one or more lines are too long

View File

@@ -9,9 +9,11 @@
*/
import type * as auth from "../auth.js";
import type * as crons from "../crons.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 files from "../files.js";
import type * as http from "../http.js";
import type * as utils from "../utils.js";
@@ -31,9 +33,11 @@ import type {
*/
declare const fullApi: ApiFromModules<{
auth: typeof auth;
crons: typeof crons;
"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;
files: typeof files;
http: typeof http;
utils: typeof utils;
}>;

View File

@@ -14,10 +14,7 @@ import { action, mutation, query } from './_generated/server';
import { Password, validatePassword } from './custom/auth';
export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({
providers: [
Authentik({ allowDangerousEmailAccountLinking: true }),
Password,
],
providers: [Authentik({ allowDangerousEmailAccountLinking: true }), Password],
});
const getUserById = async (
@@ -39,9 +36,8 @@ const isSignedIn = async (ctx: QueryCtx): Promise<Doc<'users'> | null> => {
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.');
const userId = args.userId ?? (await getAuthUserId(ctx));
if (!userId) return null;
return getUserById(ctx, userId);
},
});

View File

@@ -0,0 +1,23 @@
import { cronJobs } from 'convex/server';
import { api } from './_generated/api';
// Cron order: Minute Hour DayOfMonth Month DayOfWeek
const crons = cronJobs();
/* Example cron jobs
crons.cron(
// Run at 7:00 AM CST / 8:00 AM CDT
// Only on weekdays
'Schedule Automatic Lunches',
'0 13 * * 1-5',
api.statuses.automaticLunch,
);
crons.cron(
// Run at 4:00 PM CST / 5:00 PM CDT
// Only on weekdays
'End of shift (weekdays 5pm CT)',
'0 22 * * 1-5',
api.statuses.endOfShiftUpdate,
);
*/
export default crons;

View File

@@ -1,5 +1,6 @@
import { Password as DefaultPassword } from '@convex-dev/auth/providers/Password';
import { ConvexError } from 'convex/values';
import { UseSendOTP, UseSendOTPPasswordReset } from '..';
import { DataModel } from '../../../_generated/dataModel';

View File

@@ -0,0 +1,18 @@
import { getAuthUserId } from '@convex-dev/auth/server';
import { ConvexError, v } from 'convex/values';
import { mutation, query } from './_generated/server';
export const generateUploadUrl = mutation(async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
return await ctx.storage.generateUploadUrl();
});
export const getImageUrl = query({
args: { storageId: v.id('_storage') },
handler: async (ctx, { storageId }) => {
const url = await ctx.storage.getUrl(storageId);
return url ?? null;
},
});

View File

@@ -1,6 +1,6 @@
import { authTables } from '@convex-dev/auth/server';
import { defineSchema, defineTable } from 'convex/server';
import { v } from 'convex/values';
import { authTables } from '@convex-dev/auth/server';
const applicationTables = {
// Users contains name image & email.
@@ -10,8 +10,7 @@ const applicationTables = {
profiles: defineTable({
userId: v.id('users'),
theme_preference: v.optional(v.string()),
})
.index('userId', ['userId'])
}).index('userId', ['userId']),
};
export default defineSchema({
@@ -29,8 +28,8 @@ export default defineSchema({
phoneVerificationTime: v.optional(v.number()),
isAnonymous: v.optional(v.boolean()),
})
.index("email", ["email"])
.index('email', ['email'])
.index('name', ['name'])
.index("phone", ["phone"]),
.index('phone', ['phone']),
...applicationTables,
});

View File

@@ -7,7 +7,9 @@
"author": "Gib",
"license": "MIT",
"exports": {
"./types" : "./types/index.ts"
"./convex": "./convex/",
"./convex/*": "./convex/*",
"./types": "./types/index.ts"
},
"scripts": {
"dev": "bun with-env convex dev",

View File

@@ -1,17 +1,16 @@
#!/usr/bin/env node
import { exportJWK, exportPKCS8, generateKeyPair } from 'jose';
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
const keys = await generateKeyPair("RS256", {
const keys = await generateKeyPair('RS256', {
extractable: true,
});
const privateKey = await exportPKCS8(keys.privateKey);
const publicKey = await exportJWK(keys.publicKey);
const jwks = JSON.stringify({ keys: [{ use: "sig", ...publicKey }] });
const jwks = JSON.stringify({ keys: [{ use: 'sig', ...publicKey }] });
process.stdout.write(
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, ' ')}"`,
);
process.stdout.write("\n");
process.stdout.write('\n');
process.stdout.write(`JWKS=${jwks}`);
process.stdout.write("\n");
process.stdout.write('\n');

View File

@@ -1,5 +1 @@
export {
PASSWORD_MIN,
PASSWORD_MAX,
PASSWORD_REGEX,
} from './auth';
export { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from './auth';

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,11 @@
'use client';
import type { ComponentProps } from 'react';
import { cn, AvatarImage } from '@gib/ui';
import * as AvatarPrimitive from '@radix-ui/react-avatar';
import { User } from 'lucide-react';
import { AvatarImage, cn } from '@gib/ui';
type BasedAvatarProps = ComponentProps<typeof AvatarPrimitive.Root> & {
src?: string | null;
fullName?: string | null;

View File

@@ -1,9 +1,10 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '@gib/ui';
type BasedProgressProps = React.ComponentProps<
typeof ProgressPrimitive.Root
> & {

View File

@@ -1,9 +1,10 @@
import type { VariantProps } from 'class-variance-authority';
import * as React from 'react';
import { cn } from '@gib/ui';
import { Slot } from '@radix-ui/react-slot';
import { cva } from 'class-variance-authority';
import { cn } from '@gib/ui';
const buttonVariants = cva(
"focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
{

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { cn } from '@gib/ui';
function Card({ className, ...props }: React.ComponentProps<'div'>) {

View File

@@ -1,10 +1,11 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
import { CheckIcon } from 'lucide-react';
import { cn } from '@gib/ui';
function Checkbox({
className,
...props

View File

@@ -1,9 +1,10 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import { Drawer as DrawerPrimitive } from 'vaul';
import { cn } from '@gib/ui';
function Drawer({
...props
}: React.ComponentProps<typeof DrawerPrimitive.Root>) {

View File

@@ -1,10 +1,11 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { CheckIcon, ChevronRightIcon, CircleIcon } from 'lucide-react';
import { cn } from '@gib/ui';
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {

View File

@@ -2,9 +2,10 @@
import type { VariantProps } from 'class-variance-authority';
import { useMemo } from 'react';
import { cn, Label, Separator } from '@gib/ui';
import { cva } from 'class-variance-authority';
import { cn, Label, Separator } from '@gib/ui';
export function FieldSet({
className,
...props

View File

@@ -1,9 +1,8 @@
'use client';
import type * as LabelPrimitive from '@radix-ui/react-label';
import type { ControllerProps, FieldPath, FieldValues } from 'react-hook-form';
import * as React from 'react';
import { cn, Label } from '@gib/ui';
import type * as LabelPrimitive from '@radix-ui/react-label';
import { Slot } from '@radix-ui/react-slot';
import {
Controller,
@@ -12,6 +11,8 @@ import {
useFormState,
} from 'react-hook-form';
import { cn, Label } from '@gib/ui';
const Form = FormProvider;
type FormFieldContextValue<

View File

@@ -1,10 +1,11 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import { OTPInput, OTPInputContext } from 'input-otp';
import { MinusIcon } from 'lucide-react';
import { cn } from '@gib/ui';
function InputOTP({
className,
containerClassName,

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { cn } from '@gib/ui';
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {

View File

@@ -1,9 +1,10 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import * as LabelPrimitive from '@radix-ui/react-label';
import { cn } from '@gib/ui';
function Label({
className,
...props

View File

@@ -1,12 +1,13 @@
import type * as React from 'react';
import type { Button } from '@gib/ui';
import { cn, buttonVariants } from '@gib/ui';
import {
ChevronLeftIcon,
ChevronRightIcon,
MoreHorizontalIcon,
} from 'lucide-react';
import type { Button } from '@gib/ui';
import { buttonVariants, cn } from '@gib/ui';
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
return (
<nav

View File

@@ -1,9 +1,10 @@
'use client';
import type * as React from 'react';
import { cn } from '@gib/ui';
import * as ProgressPrimitive from '@radix-ui/react-progress';
import { cn } from '@gib/ui';
function Progress({
className,
value,

View File

@@ -1,9 +1,10 @@
'use client';
import type * as React from 'react';
import { cn } from '@gib/ui';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import { cn } from '@gib/ui';
function ScrollArea({
className,
children,

View File

@@ -1,9 +1,10 @@
'use client';
import * as React from 'react';
import { cn } from '@gib/ui';
import * as SeparatorPrimitive from '@radix-ui/react-separator';
import { cn } from '@gib/ui';
function Separator({
className,
orientation = 'horizontal',

View File

@@ -17,11 +17,12 @@ import {
useRef,
useState,
} from 'react';
import { cn, Button } from '@gib/ui';
import { CropIcon, RotateCcwIcon } from 'lucide-react';
import { Slot } from 'radix-ui';
import ReactCrop, { centerCrop, makeAspectCrop } from 'react-image-crop';
import { Button, cn } from '@gib/ui';
import 'react-image-crop/dist/ReactCrop.css';
const centerAspectCrop = (
@@ -110,7 +111,7 @@ interface ImageCropContextType {
onImageLoad: (e: SyntheticEvent<HTMLImageElement>) => void;
applyCrop: () => Promise<void>;
resetCrop: () => void;
};
}
const ImageCropContext = createContext<ImageCropContextType | null>(null);

View File

@@ -1,7 +1,7 @@
'use client';
import { useTheme } from 'next-themes';
import type { ToasterProps } from 'sonner';
import { useTheme } from 'next-themes';
import { Toaster as Sonner } from 'sonner';
const Toaster = ({ ...props }: ToasterProps) => {

View File

@@ -1,4 +1,5 @@
import type { ComponentProps } from 'react';
import { cn } from '@gib/ui';
type Message = { success: string } | { error: string } | { message: string };
@@ -7,7 +8,7 @@ interface StatusMessageProps {
message: Message;
containerProps?: ComponentProps<'div'>;
textProps?: ComponentProps<'div'>;
};
}
export const StatusMessage = ({
message,

View File

@@ -1,10 +1,11 @@
'use client';
import type { ComponentProps } from 'react';
import { cn, Button } from '@gib/ui';
import { Loader2 } from 'lucide-react';
import { useFormStatus } from 'react-dom';
import { Button, cn } from '@gib/ui';
export type SubmitButtonProps = Omit<
ComponentProps<typeof Button>,
'type' | 'aria-disabled'

View File

@@ -1,9 +1,10 @@
'use client';
import type * as React from 'react';
import { cn } from '@gib/ui';
import * as SwitchPrimitive from '@radix-ui/react-switch';
import { cn } from '@gib/ui';
function Switch({
className,
...props

View File

@@ -1,6 +1,7 @@
'use client';
import type * as React from 'react';
import { cn } from '@gib/ui';
function Table({ className, ...props }: React.ComponentProps<'table'>) {

View File

@@ -1,9 +1,10 @@
'use client';
import type * as React from 'react';
import { cn } from '@gib/ui';
import * as TabsPrimitive from '@radix-ui/react-tabs';
import { cn } from '@gib/ui';
function Tabs({
className,
...props

View File

@@ -24,7 +24,7 @@ const ThemeProvider = ({
interface ThemeToggleProps {
size?: number;
buttonProps?: Omit<ComponentProps<typeof Button>, 'onClick'>;
};
}
const ThemeToggle = ({ size = 1, buttonProps }: ThemeToggleProps) => {
const { setTheme, resolvedTheme } = useTheme();