Made great progress on monorepo & auth for next. Very happy with work!
This commit is contained in:
File diff suppressed because one or more lines are too long
4
packages/backend/convex/_generated/api.d.ts
vendored
4
packages/backend/convex/_generated/api.d.ts
vendored
@@ -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;
|
||||
}>;
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
|
||||
23
packages/backend/convex/crons.ts
Normal file
23
packages/backend/convex/crons.ts
Normal 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;
|
||||
@@ -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';
|
||||
|
||||
|
||||
18
packages/backend/convex/files.ts
Normal file
18
packages/backend/convex/files.ts
Normal 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;
|
||||
},
|
||||
});
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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
|
||||
> & {
|
||||
|
||||
@@ -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",
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Card({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import type * as React from 'react';
|
||||
|
||||
import { cn } from '@gib/ui';
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user