authentik now working with convex auth

This commit is contained in:
2026-01-12 10:10:03 -06:00
parent abcf9df6aa
commit 72f11f0b02
27 changed files with 1310 additions and 134 deletions

281
AGENTS.md Normal file
View File

@@ -0,0 +1,281 @@
# AGENTS.md - Convex Turbo Monorepo
## Quick Reference
### Build/Lint/Test Commands
```bash
# Development
bun dev # Run all apps (Next.js + Expo + Backend)
bun dev:next # Run Next.js + Convex backend only
bun dev:expo # Run Expo + Convex backend only
bun dev:backend # Run Convex backend only
bun dev:expo:tunnel # Expo with tunnel for physical device
# Quality
bun lint # Lint all packages
bun lint:fix # Lint and auto-fix
bun format # Check formatting
bun format:fix # Fix formatting
bun typecheck # TypeScript type checking
# Build
bun build # Build all packages
# Single Package Commands (use Turborepo filters)
bun turbo run dev -F @gib/next # Single app dev
bun turbo run lint -F @gib/backend # Lint single package
bun turbo run typecheck -F @gib/ui # Typecheck single package
# Cleanup
bun clean # Clean all node_modules (git clean)
bun clean:ws # Clean workspace caches
```
## Project Structure
```
convex-monorepo/
├── apps/
│ ├── next/ # Next.js 16 web app (@gib/next)
│ └── expo/ # Expo 54 mobile app (@gib/expo)
├── packages/
│ ├── backend/ # Convex backend (@gib/backend)
│ │ └── convex/ # Convex functions, schema, auth
│ └── ui/ # Shared shadcn/ui components (@gib/ui)
├── tools/
│ ├── eslint/ # @gib/eslint-config
│ ├── prettier/ # @gib/prettier-config
│ ├── tailwind/ # @gib/tailwind-config
│ └── typescript/ # @gib/tsconfig
├── docker/ # Self-hosted Convex deployment
└── .env # Central environment variables
```
## Dependency Management
### Catalogs (Single Source of Truth)
All shared dependencies are defined in root `package.json` catalogs:
```json
"catalog": {
"prettier": "^3.6.2",
"typescript": "^5.9.3",
"eslint": "^9.38.0"
},
"catalogs": {
"convex": { "convex": "^1.28.0", "@convex-dev/auth": "^0.0.81" },
"react19": { "react": "^19.1.4", "react-dom": "19.1.4" }
}
```
### Using Catalogs in Packages
```json
"dependencies": {
"convex": "catalog:convex",
"react": "catalog:react19",
"typescript": "catalog:"
},
"devDependencies": {
"@gib/eslint-config": "workspace:*",
"@gib/ui": "workspace:*"
}
```
### Updating Dependencies
**IMPORTANT:** Do NOT use `bun update` directly - it may replace catalog: with versions.
```bash
# Correct workflow:
1. Edit version in root package.json catalog section
2. Run: bun install
3. Verify with: bun lint:ws (runs sherif)
```
## Code Style Guidelines
### Imports (via @gib/prettier-config)
```typescript
// Order: Types → React → Next/Expo → Third-party → @gib/* → Local
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { api } from '@/convex/_generated/api';
import { ConvexError } from 'convex/values';
import { cn } from '@gib/ui';
import type { User } from './types';
```
### TypeScript
- Strict mode enabled (`noUncheckedIndexedAccess: true`)
- Use `type` imports: `import type { X } from 'y'`
- Prefix unused vars with `_`: `const _unused = ...`
- Avoid `any` - use `unknown` with type guards
### Naming Conventions
- Components: `PascalCase` (`UserProfile.tsx`)
- Functions/variables: `camelCase`
- Constants: `SCREAMING_SNAKE_CASE`
- Files: `kebab-case.ts` (except components)
### Error Handling
```typescript
// Convex functions - use ConvexError
import { ConvexError } from 'convex/values';
throw new ConvexError('User not found.');
// Client-side - handle gracefully
try { ... } catch (e) {
if (e instanceof ConvexError) { /* handle */ }
}
```
### Formatting
- Single quotes, trailing commas
- 80 char line width, 2 space indent
- Tailwind classes sorted via prettier-plugin-tailwindcss
- Use `cn()` for conditional classes: `cn('base', condition && 'active')`
## Environment Variables
### Central .env (root)
All env vars in `/.env`. Apps load via `with-env` script:
```json
"scripts": {
"dev": "bun with-env next dev --turbo",
"with-env": "dotenv -e ../../.env --"
}
```
### Required Variables
```bash
# Convex
CONVEX_SELF_HOSTED_URL=https://api.convex.example.com
CONVEX_SELF_HOSTED_ADMIN_KEY=<generated>
CONVEX_SITE_URL=https://convex.example.com
NEXT_PUBLIC_CONVEX_URL=https://api.convex.example.com
# Auth (sync to Convex deployment)
AUTH_AUTHENTIK_ID=
AUTH_AUTHENTIK_SECRET=
AUTH_AUTHENTIK_ISSUER=
USESEND_API_KEY=
```
### Syncing to Convex Deployment
```bash
# Upload env vars to self-hosted Convex
npx convex env set AUTH_AUTHENTIK_ID "value"
npx convex env set AUTH_AUTHENTIK_SECRET "value"
npx convex env set AUTH_AUTHENTIK_ISSUER "value"
npx convex env set USESEND_API_KEY "value"
npx convex env set CONVEX_SITE_URL "https://convex.example.com"
```
## Convex Backend Patterns
### Schema (`packages/backend/convex/schema.ts`)
```typescript
import { defineSchema, defineTable } from 'convex/server';
import { authTables } from '@convex-dev/auth/server';
export default defineSchema({
...authTables,
users: defineTable({ ... }).index('email', ['email']),
});
```
### Queries & Mutations
```typescript
import { v } from 'convex/values';
import { mutation, query } from './_generated/server';
export const getUser = query({
args: { userId: v.id('users') },
handler: async (ctx, args) => {
return await ctx.db.get(args.userId);
},
});
```
### Auth
```typescript
import { getAuthUserId } from '@convex-dev/auth/server';
// In any query/mutation:
const userId = await getAuthUserId(ctx);
if (!userId) throw new ConvexError('Not authenticated.');
```
## Next.js Patterns
### Convex Provider Setup
```tsx
// src/components/providers/ConvexClientProvider.tsx
'use client';
import { ConvexAuthNextjsProvider } from '@convex-dev/auth/nextjs';
import { ConvexReactClient } from 'convex/react';
const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
export const ConvexClientProvider = ({ children }) => (
<ConvexAuthNextjsProvider client={convex}>
{children}
</ConvexAuthNextjsProvider>
);
```
### Path Aliases
- `@/*``./src/*` (Next.js)
- `~/*``./src/*` (Expo)
## Expo Patterns
### NativeWind (Tailwind for RN)
```tsx
import '../styles.css';
// Use className like web: <View className="flex-1 bg-background" />
```
### Secure Storage for Auth
```typescript
import * as SecureStore from 'expo-secure-store';
// Tokens stored via SecureStore, not AsyncStorage
```
## Known Issues / Cleanup Needed
1. **Apps reference @acme/_ instead of @gib/_** - Legacy T3 template imports
2. **TRPC imports exist but no TRPC** - Replace with Convex client
3. **Expo uses better-auth** - Should use @convex-dev/auth
4. **@gib/ui uses pnpm dlx** - Should use bunx for shadcn
## Adding UI Components
```bash
bun ui-add # Interactive shadcn/ui component addition
```

View File

@@ -1,11 +1,6 @@
import { withSentryConfig } from '@sentry/nextjs'; import { withSentryConfig } from '@sentry/nextjs';
import { createJiti } from 'jiti';
import { withPlausibleProxy } from 'next-plausible'; import { withPlausibleProxy } from 'next-plausible';
import { env } from './src/env.js';
const jiti = createJiti(import.meta.url);
// Import env files to validate at build time. Use jiti so we can load .ts files in here.
await jiti.import('./src/env');
/** @type {import("next").NextConfig} */ /** @type {import("next").NextConfig} */
const config = withPlausibleProxy({ const config = withPlausibleProxy({

View File

@@ -38,7 +38,6 @@
"@types/react": "catalog:react19", "@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19", "@types/react-dom": "catalog:react19",
"eslint": "catalog:", "eslint": "catalog:",
"jiti": "^2.5.1",
"prettier": "catalog:", "prettier": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -0,0 +1,460 @@
'use client';
import { z } from 'zod';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import Link from 'next/link';
import { useAuthActions } from '@convex-dev/auth/react';
import { useRouter } from 'next/navigation';
import { ConvexError } from 'convex/values';
import { useState } from 'react';
import {
Card,
CardContent,
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
Input,
InputOTP,
InputOTPGroup,
InputOTPSlot,
InputOTPSeparator,
Separator,
SubmitButton,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '@gib/ui';
import { toast } from 'sonner';
import {
GibsAuthSignInButton,
} from '@/components/layout/auth/buttons';
import { PASSWORD_MIN, PASSWORD_MAX, PASSWORD_REGEX } from '@gib/backend/types';
const signInFormSchema = z.object({
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z.string().regex(PASSWORD_REGEX, {
message: 'Incorrect password. Does not meet requirements.',
}),
});
const signUpFormSchema = z
.object({
name: z.string().min(2, {
message: 'Name must be at least 2 characters.',
}),
email: z.email({
message: 'Please enter a valid email address.',
}),
password: z
.string()
.min(PASSWORD_MIN, {
message: `Password must be at least ${PASSWORD_MIN} characters.`,
})
.max(PASSWORD_MAX, {
message: `Password must be no more than ${PASSWORD_MAX} characters.`,
})
.regex(/^\S+$/, {
message: 'Password must not contain whitespace.',
})
.regex(/[0-9]/, {
message: 'Password must contain at least one digit.',
})
.regex(/[a-z]/, {
message: 'Password must contain at least one lowercase letter.',
})
.regex(/[A-Z]/, {
message: 'Password must contain at least one uppercase letter.',
})
.regex(/[\p{P}\p{S}]/u, {
message: 'Password must contain at least one symbol.',
}),
confirmPassword: z.string().min(PASSWORD_MIN, {
message: `Password must be at least ${PASSWORD_MIN} characters.`,
}),
})
.refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match!',
path: ['confirmPassword'],
});
const verifyEmailFormSchema = z.object({
code: z.string({ message: 'Invalid code.' }),
});
const SignIn = () => {
const { signIn } = useAuthActions();
const [flow, setFlow] = useState<'signIn' | 'signUp' | 'email-verification'>(
'signIn',
);
const [email, setEmail] = useState<string>('');
const [code, setCode] = useState<string>('');
const [loading, setLoading] = useState(false);
const router = useRouter();
const signInForm = useForm<z.infer<typeof signInFormSchema>>({
resolver: zodResolver(signInFormSchema),
defaultValues: { email: '', password: '' },
});
const signUpForm = useForm<z.infer<typeof signUpFormSchema>>({
resolver: zodResolver(signUpFormSchema),
defaultValues: {
name: '',
email,
password: '',
confirmPassword: '',
},
});
const verifyEmailForm = useForm<z.infer<typeof verifyEmailFormSchema>>({
resolver: zodResolver(verifyEmailFormSchema),
defaultValues: { code },
});
const handleSignIn = async (values: z.infer<typeof signInFormSchema>) => {
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
formData.append('flow', flow);
setLoading(true);
try {
await signIn('password', formData).then(() => router.push('/'));
} catch (error) {
console.error('Error signing in:', error);
toast.error('Error signing in.');
} finally {
signInForm.reset();
setLoading(false);
}
};
const handleSignUp = async (values: z.infer<typeof signUpFormSchema>) => {
const formData = new FormData();
formData.append('email', values.email);
formData.append('password', values.password);
formData.append('flow', flow);
formData.append('name', values.name);
setLoading(true);
try {
if (values.confirmPassword !== values.password)
throw new ConvexError('Passwords do not match.');
await signIn('password', formData).then(() => {
setEmail(values.email);
setFlow('email-verification');
});
} catch (error) {
console.error('Error signing up:', error);
toast.error('Error signing up.');
} finally {
signUpForm.reset();
setLoading(false);
}
};
const handleVerifyEmail = async (
values: z.infer<typeof verifyEmailFormSchema>,
) => {
const formData = new FormData();
formData.append('code', code);
formData.append('flow', flow);
formData.append('email', email);
setLoading(true);
try {
await signIn('password', formData).then(() => router.push('/'));
} catch (error) {
console.error('Error verifying email:', error);
toast.error('Error verifying email.');
} finally {
verifyEmailForm.reset();
setLoading(false);
}
};
if (flow === 'email-verification') {
return (
<div className='flex flex-col items-center'>
<Card className='p-4 bg-card/25 min-h-[720px] w-md'>
<CardContent>
<div className='text-center mb-6'>
<h2 className='text-2xl font-bold'>Verify Your Email</h2>
<p className='text-muted-foreground'>We sent a code to {email}</p>
</div>
<Form {...verifyEmailForm}>
<form
onSubmit={verifyEmailForm.handleSubmit(handleVerifyEmail)}
className='flex flex-col space-y-8'
>
<FormField
control={verifyEmailForm.control}
name='code'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>Code</FormLabel>
<FormControl>
<InputOTP
maxLength={6}
value={code}
onChange={(value) => setCode(value)}
>
<InputOTPGroup>
<InputOTPSlot index={0} />
<InputOTPSlot index={1} />
<InputOTPSlot index={2} />
<InputOTPSeparator />
<InputOTPSlot index={3} />
<InputOTPSlot index={4} />
<InputOTPSlot index={5} />
</InputOTPGroup>
</InputOTP>
</FormControl>
<FormDescription>
Please enter the one-time password sent to your email.
</FormDescription>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<SubmitButton
disabled={loading}
pendingText='Signing Up...'
className='text-xl font-semibold w-2/3 mx-auto'
>
Verify Email
</SubmitButton>
</form>
</Form>
<div className='text-center mt-4'>
<button
onClick={() => setFlow('signUp')}
className='text-sm text-muted-foreground hover:underline'
>
Back to Sign Up
</button>
</div>
</CardContent>
</Card>
</div>
);
}
return (
<div className='flex flex-col items-center'>
<Card className='p-4 bg-card/25 min-h-[720px] w-md'>
<Tabs
defaultValue={flow}
onValueChange={(value) => setFlow(value as 'signIn' | 'signUp')}
className='items-center'
>
<TabsList className='py-6'>
<TabsTrigger
value='signIn'
className='p-6 text-2xl font-bold cursor-pointer'
>
Sign In
</TabsTrigger>
<TabsTrigger
value='signUp'
className='p-6 text-2xl font-bold cursor-pointer'
>
Sign Up
</TabsTrigger>
</TabsList>
<TabsContent value='signIn'>
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
<CardContent>
<Form {...signInForm}>
<form
onSubmit={signInForm.handleSubmit(handleSignIn)}
className='flex flex-col space-y-8'
>
<FormField
control={signInForm.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>Email</FormLabel>
<FormControl>
<Input
type='email'
placeholder='you@example.com'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<FormField
control={signInForm.control}
name='password'
render={({ field }) => (
<FormItem>
<div className='flex justify-between'>
<FormLabel className='text-xl'>Password</FormLabel>
<Link href='/forgot-password'>
Forgot Password?
</Link>
</div>
<FormControl>
<Input
type='password'
placeholder='Your password'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<SubmitButton
disabled={loading}
pendingText='Signing in...'
className='text-xl font-semibold w-2/3 mx-auto'
>
Sign In
</SubmitButton>
</form>
</Form>
<div className='flex justify-center'>
<div
className='flex flex-row items-center
my-2.5 mx-auto justify-center w-1/4'
>
<Separator className='py-0.5 mr-3' />
<span className='font-semibold text-lg'>or</span>
<Separator className='py-0.5 ml-3' />
</div>
</div>
<div className='flex justify-center mt-3'>
<GibsAuthSignInButton />
</div>
</CardContent>
</Card>
</TabsContent>
<TabsContent value='signUp'>
<Card className='min-w-xs sm:min-w-sm bg-card/50'>
<CardContent>
<Form {...signUpForm}>
<form
onSubmit={signUpForm.handleSubmit(handleSignUp)}
className='flex flex-col space-y-8'
>
<FormField
control={signUpForm.control}
name='name'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>Name</FormLabel>
<FormControl>
<Input
type='text'
placeholder='Full Name'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<FormField
control={signUpForm.control}
name='email'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>Email</FormLabel>
<FormControl>
<Input
type='email'
placeholder='you@example.com'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<FormField
control={signUpForm.control}
name='password'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>Password</FormLabel>
<FormControl>
<Input
type='password'
placeholder='Your password'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<FormField
control={signUpForm.control}
name='confirmPassword'
render={({ field }) => (
<FormItem>
<FormLabel className='text-xl'>
Confirm Passsword
</FormLabel>
<FormControl>
<Input
type='password'
placeholder='Confirm your password'
{...field}
/>
</FormControl>
<div className='flex flex-col w-full items-center'>
<FormMessage className='w-5/6 text-center' />
</div>
</FormItem>
)}
/>
<SubmitButton
disabled={loading}
pendingText='Signing Up...'
className='text-xl font-semibold w-2/3 mx-auto'
>
Sign Up
</SubmitButton>
</form>
</Form>
<div className='flex my-auto justify-center w-2/3'>
<div className='flex flex-row w-1/3 items-center my-2.5'>
<Separator className='py-0.5 mr-3' />
<span className='font-semibold text-lg'>or</span>
<Separator className='py-0.5 ml-3' />
</div>
</div>
<div className='flex justify-center mt-3'>
<GibsAuthSignInButton type='signUp' />
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</Card>
</div>
);
};
export default SignIn;

View File

@@ -1,34 +1,16 @@
import type { Metadata, Viewport } from 'next'; import type { Metadata, Viewport } from 'next';
import { Geist, Geist_Mono } from 'next/font/google'; import { Geist, Geist_Mono } from 'next/font/google';
import { cn } from '@acme/ui';
import { ThemeProvider, ThemeToggle } from '@acme/ui/theme';
import { Toaster } from '@acme/ui/toast';
import { env } from '~/env'; import '@/app/styles.css';
import { TRPCReactProvider } from '~/trpc/react';
import '~/app/styles.css'; import { ConvexClientProvider } from '@/components/providers';
import { generateMetadata } from '@/lib/metadata';
import { ConvexAuthNextjsServerProvider } from '@convex-dev/auth/nextjs/server';
import PlausibleProvider from 'next-plausible';
export const metadata: Metadata = { import { ThemeProvider, Toaster } from '@gib/ui';
metadataBase: new URL(
env.VERCEL_ENV === 'production' export const metadata: Metadata = generateMetadata();
? 'https://turbo.t3.gg'
: 'http://localhost:3000',
),
title: 'Create T3 Turbo',
description: 'Simple monorepo with shared backend for web & mobile apps',
openGraph: {
title: 'Create T3 Turbo',
description: 'Simple monorepo with shared backend for web & mobile apps',
url: 'https://create-t3-turbo.vercel.app',
siteName: 'Create T3 Turbo',
},
twitter: {
card: 'summary_large_image',
site: '@jullerino',
creator: '@jullerino',
},
};
export const viewport: Viewport = { export const viewport: Viewport = {
themeColor: [ themeColor: [
@@ -46,24 +28,36 @@ const geistMono = Geist_Mono({
variable: '--font-geist-mono', variable: '--font-geist-mono',
}); });
export default function RootLayout(props: { children: React.ReactNode }) { const RootLayout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
return ( return (
<html lang="en" suppressHydrationWarning> <ConvexAuthNextjsServerProvider>
<body <PlausibleProvider
className={cn( domain="convexmonorepo.gbrown.org"
'bg-background text-foreground min-h-screen font-sans antialiased', customDomain="https://plausible.gbrown.org"
geistSans.variable,
geistMono.variable,
)}
> >
<ThemeProvider> <html lang="en">
<TRPCReactProvider>{props.children}</TRPCReactProvider> <body
<div className="absolute right-4 bottom-4"> className={`${geistSans.variable} ${geistMono.variable} antialiased`}
<ThemeToggle /> >
</div> <ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<ConvexClientProvider>
{children}
<Toaster /> <Toaster />
</ConvexClientProvider>
</ThemeProvider> </ThemeProvider>
</body> </body>
</html> </html>
</PlausibleProvider>
</ConvexAuthNextjsServerProvider>
); );
} };
export default RootLayout;

View File

@@ -1,41 +1,11 @@
import { Suspense } from 'react'; 'use client';
import { HydrateClient, prefetch, trpc } from '~/trpc/server';
import { AuthShowcase } from './_components/auth-showcase';
import {
CreatePostForm,
PostCardSkeleton,
PostList,
} from './_components/posts';
export default function HomePage() {
prefetch(trpc.post.all.queryOptions());
const Home = () => {
return ( return (
<HydrateClient> <div className="flex min-h-screen items-center justify-center">
<main className="container h-screen py-16"> Hello!
<div className="flex flex-col items-center justify-center gap-4">
<h1 className="text-5xl font-extrabold tracking-tight sm:text-[5rem]">
Create <span className="text-primary">T3</span> Turbo
</h1>
<AuthShowcase />
<CreatePostForm />
<div className="w-full max-w-2xl overflow-y-scroll">
<Suspense
fallback={
<div className="flex w-full flex-col gap-4">
<PostCardSkeleton />
<PostCardSkeleton />
<PostCardSkeleton />
</div> </div>
}
>
<PostList />
</Suspense>
</div>
</div>
</main>
</HydrateClient>
); );
} };
export default Home;

View File

@@ -1,6 +1,6 @@
@import 'tailwindcss'; @import 'tailwindcss';
@import 'tw-animate-css'; @import 'tw-animate-css';
@import '@acme/tailwind-config/theme'; @import '@gib/tailwind-config/theme';
@source '../../../../packages/ui/src/*.{ts,tsx}'; @source '../../../../packages/ui/src/*.{ts,tsx}';

View File

@@ -0,0 +1,46 @@
import Image from 'next/image';
import { useAuthActions } from '@convex-dev/auth/react';
import type { buttonVariants } from '@gib/ui';
import { Button } from '@gib/ui';
import type { ComponentProps } from 'react';
import type { VariantProps } from 'class-variance-authority';
interface Props {
buttonProps?: Omit<ComponentProps<'button'>, 'onClick'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
};
type?: 'signIn' | 'signUp';
};
export const GibsAuthSignInButton = ({
buttonProps,
type = 'signIn',
}: Props) => {
const { signIn } = useAuthActions();
return (
<Button
size='lg'
onClick={() => signIn('authentik')}
className='text-lg font-semibold'
{...buttonProps}
>
<div className='flex flex-col my-auto space-x-1'>
<p>
{
type === 'signIn'
? 'Sign In'
: 'Sign Up'
} with
</p>
<Image
src={'/misc/auth/gibs_auth_wide_header.png'}
className=''
alt="Gib's Auth"
width={100}
height={100}
/>
</div>
</Button>
);
};

View File

@@ -0,0 +1 @@
export { GibsAuthSignInButton } from './gibs-auth';

View File

@@ -0,0 +1,16 @@
"use client";
import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs";
import { ConvexReactClient } from "convex/react";
import type { ReactNode } from "react";
import { env } from '@/env';
const convex = new ConvexReactClient(env.NEXT_PUBLIC_CONVEX_URL);
export function ConvexClientProvider({ children }: { children: ReactNode }) {
return (
<ConvexAuthNextjsProvider client={convex}>
{children}
</ConvexAuthNextjsProvider>
);
}

View File

@@ -0,0 +1 @@
export { ConvexClientProvider } from './ConvexClientProvider';

View File

@@ -0,0 +1,129 @@
import type { Metadata } from 'next';
import * as Sentry from '@sentry/nextjs';
export const generateMetadata = (): Metadata => {
return {
title: {
template: '%s | Convex Monorepo',
default: 'Convex Monorepo',
},
description: 'A Convex Monorepo with Next.js & Expo',
applicationName: 'Convex Monorepo',
keywords:
'Convex Monorepo,T3 Template, Nextjs, ' +
'Tailwind, TypeScript, React, T3, Gib',
authors: [{ name: 'Gib', url: 'https://gbrown.org' }],
creator: 'Gib Brown',
publisher: 'Gib Brown',
formatDetection: {
email: false,
address: false,
telephone: false,
},
robots: {
index: true,
follow: true,
nocache: false,
googleBot: {
index: true,
follow: true,
noimageindex: false,
'max-video-preview': -1,
'max-image-preview': 'large',
'max-snippet': -1,
},
},
icons: {
icon: [
{ url: '/favicon.ico', type: 'image/x-icon', sizes: 'any' },
{
url: '/favicon.ico',
type: 'image/x-icon',
sizes: 'any',
media: '(prefers-color-scheme: dark)',
},
//{
//url: '/appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//},
//{
//url: '/appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//media: '(prefers-color-scheme: dark)',
//},
],
//shortcut: [
//{
//url: '/appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//},
//{
//url: '/appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//media: '(prefers-color-scheme: dark)',
//},
//],
//apple: [
//{
//url: 'appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//},
//{
//url: 'appicon/icon.png',
//type: 'image/png',
//sizes: '192x192',
//media: '(prefers-color-scheme: dark)',
//},
//],
//other: [
//{
//rel: 'apple-touch-icon-precomposed',
//url: '/appicon/icon-precomposed.png',
//type: 'image/png',
//sizes: '180x180',
//},
//],
},
other: {
...Sentry.getTraceData(),
},
//appleWebApp: {
//title: 'Convex Monorepo',
//statusBarStyle: 'black-translucent',
//startupImage: [
//'/icons/apple/splash-768x1004.png',
//{
//url: '/icons/apple/splash-1536x2008.png',
//media: '(device-width: 768px) and (device-height: 1024px)',
//},
//],
//},
verification: {
google: 'google',
yandex: 'yandex',
yahoo: 'yahoo',
},
category: 'technology',
/*
appLinks: {
ios: {
url: 'https://techtracker.gbrown.org/ios',
app_store_id: 'com.gbrown.techtracker',
},
android: {
package: 'https://techtracker.gbrown.org/android',
app_name: 'app_t3_template',
},
web: {
url: 'https://techtracker.gbrown.org',
should_fallback: true,
},
},
*/
};
};

View File

@@ -0,0 +1,199 @@
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
// In-memory stores for tracking IPs (use Redis in production)
const ipAttempts = new Map<string, { count: number; lastAttempt: number }>();
const ip404Attempts = new Map<string, { count: number; lastAttempt: number }>();
const bannedIPs = new Set<string>();
// Suspicious patterns that indicate malicious activity
const MALICIOUS_PATTERNS = [
// Your existing patterns
/web-inf/i,
/\.jsp/i,
/\.php/i,
/puttest/i,
/WEB-INF/i,
/\.xml$/i,
/perl/i,
/xampp/i,
/phpwebgallery/i,
/FileManager/i,
/standalonemanager/i,
/h2console/i,
/WebAdmin/i,
/login_form\.php/i,
/%2e/i,
/%u002e/i,
/\.%00/i,
/\.\./,
/lcgi/i,
// New patterns from your logs
/\/appliance\//i,
/bomgar/i,
/netburner-logo/i,
/\/ui\/images\//i,
/logon_merge/i,
/logon_t\.gif/i,
/login_top\.gif/i,
/theme1\/images/i,
/\.well-known\/acme-challenge\/.*\.jpg$/i,
/\.well-known\/pki-validation\/.*\.jpg$/i,
// Path traversal and system file access patterns
/\/etc\/passwd/i,
/\/etc%2fpasswd/i,
/\/etc%5cpasswd/i,
/\/\/+etc/i,
/\\\\+.*etc/i,
/%2f%2f/i,
/%5c%5c/i,
/\/\/+/,
/\\\\+/,
/%00/i,
/%23/i,
// Encoded path traversal attempts
/%2e%2e/i,
/%252e/i,
/%c0%ae/i,
/%c1%9c/i,
];
// Suspicious HTTP methods
const SUSPICIOUS_METHODS = ['TRACE', 'PUT', 'DELETE', 'PATCH'];
const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute
const MAX_ATTEMPTS = 10; // Max suspicious requests per window
const BAN_DURATION = 30 * 60 * 1000; // 30 minutes
// 404 rate limiting settings
const RATE_404_WINDOW = 2 * 60 * 1000; // 2 minutes
const MAX_404_ATTEMPTS = 10; // Max 404s before ban
const getClientIP = (request: NextRequest): string => {
const forwarded = request.headers.get('x-forwarded-for');
const realIP = request.headers.get('x-real-ip');
const cfConnectingIP = request.headers.get('cf-connecting-ip');
if (forwarded) return (forwarded.split(',')[0] ?? '').trim();
if (realIP) return realIP;
if (cfConnectingIP) return cfConnectingIP;
return request.headers.get('host') ?? 'unknown';
};
const isPathSuspicious = (pathname: string): boolean => {
return MALICIOUS_PATTERNS.some((pattern) => pattern.test(pathname));
};
const isMethodSuspicious = (method: string): boolean => {
return SUSPICIOUS_METHODS.includes(method);
};
const updateIPAttempts = (ip: string): boolean => {
const now = Date.now();
const attempts = ipAttempts.get(ip);
if (!attempts || now - attempts.lastAttempt > RATE_LIMIT_WINDOW) {
ipAttempts.set(ip, { count: 1, lastAttempt: now });
return false;
}
attempts.count++;
attempts.lastAttempt = now;
if (attempts.count > MAX_ATTEMPTS) {
bannedIPs.add(ip);
ipAttempts.delete(ip);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
return false;
};
const update404Attempts = (ip: string): boolean => {
const now = Date.now();
const attempts = ip404Attempts.get(ip);
if (!attempts || now - attempts.lastAttempt > RATE_404_WINDOW) {
ip404Attempts.set(ip, { count: 1, lastAttempt: now });
return false;
}
attempts.count++;
attempts.lastAttempt = now;
if (attempts.count > MAX_404_ATTEMPTS) {
bannedIPs.add(ip);
ip404Attempts.delete(ip);
console.log(
`🔨 IP ${ip} banned for excessive 404 requests (${attempts.count} in ${RATE_404_WINDOW / 1000}s)`,
);
setTimeout(() => {
bannedIPs.delete(ip);
}, BAN_DURATION);
return true;
}
return false;
};
export const banSuspiciousIPs = (request: NextRequest): NextResponse | null => {
const { pathname } = request.nextUrl;
const method = request.method;
const ip = getClientIP(request);
// Check if IP is already banned
if (bannedIPs.has(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
const isSuspiciousPath = isPathSuspicious(pathname);
const isSuspiciousMethod = isMethodSuspicious(method);
// Handle suspicious activity
if (isSuspiciousPath || isSuspiciousMethod) {
const shouldBan = updateIPAttempts(ip);
if (shouldBan) {
console.log(`🔨 IP ${ip} has been banned for suspicious activity`);
return new NextResponse('Access denied - IP banned. Please fuck off.', {
status: 403,
});
}
return new NextResponse('Not Found', { status: 404 });
}
return null;
};
// Call this function when you detect a 404 response
export const handle404Response = (
request: NextRequest,
): NextResponse | null => {
const ip = getClientIP(request);
if (bannedIPs.has(ip)) {
return new NextResponse('Access denied.', { status: 403 });
}
const shouldBan = update404Attempts(ip);
if (shouldBan) {
return new NextResponse('Access denied - IP banned for excessive 404s.', {
status: 403,
});
}
return null;
};

View File

@@ -0,0 +1,34 @@
import {
convexAuthNextjsMiddleware,
createRouteMatcher,
nextjsMiddlewareRedirect,
} from '@convex-dev/auth/nextjs/server';
import { banSuspiciousIPs } from '@/lib/middleware/ban-sus-ips';
const isSignInPage = createRouteMatcher(['/sign-in']);
const isProtectedRoute = createRouteMatcher(['/', '/profile']);
export default convexAuthNextjsMiddleware(
async (request, { convexAuth }) => {
const banResponse = banSuspiciousIPs(request);
if (banResponse) return banResponse;
if (isSignInPage(request) && (await convexAuth.isAuthenticated())) {
return nextjsMiddlewareRedirect(request, '/');
}
if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) {
return nextjsMiddlewareRedirect(request, '/sign-in');
}
},
{ cookieConfig: { maxAge: 60 * 60 * 24 * 30 } },
);
export const config = {
// The following matcher runs middleware on all routes
// except static assets.
matcher: [
'/((?!_next/static|_next/image|favicon.ico|monitoring-tunnel|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
'/((?!.*\\..*|_next).*)',
'/',
'/(api|trpc)(.*)',
],
};

View File

@@ -1,12 +1,12 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "convex-turbo", "name": "convex-turbo",
"devDependencies": { "devDependencies": {
"@gib/prettier-config": "workspace:", "@gib/prettier-config": "workspace:",
"@turbo/gen": "^2.7.3", "@turbo/gen": "^2.7.3",
"baseline-browser-mapping": "^2.9.14",
"dotenv-cli": "^10.0.0", "dotenv-cli": "^10.0.0",
"prettier": "catalog:", "prettier": "catalog:",
"turbo": "^2.7.3", "turbo": "^2.7.3",
@@ -91,8 +91,8 @@
"@types/node": "catalog:", "@types/node": "catalog:",
"@types/react": "catalog:react19", "@types/react": "catalog:react19",
"@types/react-dom": "catalog:react19", "@types/react-dom": "catalog:react19",
"baseline-browser-mapping": "^2.9.14",
"eslint": "catalog:", "eslint": "catalog:",
"jiti": "^2.5.1",
"prettier": "catalog:", "prettier": "catalog:",
"tailwindcss": "catalog:", "tailwindcss": "catalog:",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
@@ -240,7 +240,7 @@
"react19": { "react19": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"@types/react-dom": "~19.1.0", "@types/react-dom": "~19.1.0",
"react": "^19.1.4", "react": "19.1.4",
"react-dom": "19.1.4", "react-dom": "19.1.4",
}, },
}, },
@@ -1605,7 +1605,7 @@
"base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="], "base64id": ["base64id@2.0.0", "", {}, "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.14", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg=="],
"basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="], "basic-ftp": ["basic-ftp@5.0.5", "", {}, "sha512-4Bcg1P8xhUuqcii/S0Z9wiHIrQVPMermM1any+MX5GeGD7faD3/msQUDGLol9wOcz4/jbg/WJnGqoJF6LiBdtg=="],
@@ -2689,7 +2689,7 @@
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="], "react": ["react@19.1.4", "", {}, "sha512-DHINL3PAmPUiK1uszfbKiXqfE03eszdt5BpVSuEAHb5nfmNPwnsy7g39h2t8aXFc/Bv99GH81s+j8dobtD+jOw=="],
"react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="],
@@ -3397,6 +3397,8 @@
"better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], "better-opn/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="],
"browserslist/baseline-browser-mapping": ["baseline-browser-mapping@2.8.21", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-JU0h5APyQNsHOlAM7HnQnPToSDQoEBZqzu/YBlqDnEeymPnZDREeXJA3KBMQee+dKteAxZ2AtvQEvVYdZf241Q=="],
"builtins/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "builtins/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="],
"chalk-template/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], "chalk-template/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="],
@@ -3623,6 +3625,8 @@
"update-browserslist-db/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "update-browserslist-db/picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"usesend-js/react": ["react@19.2.0", "", {}, "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ=="],
"webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], "webpack/eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="],
"webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],

View File

@@ -28,7 +28,7 @@
"react19": { "react19": {
"@types/react": "~19.1.0", "@types/react": "~19.1.0",
"@types/react-dom": "~19.1.0", "@types/react-dom": "~19.1.0",
"react": "^19.1.4", "react": "19.1.4",
"react-dom": "19.1.4" "react-dom": "19.1.4"
} }
}, },
@@ -56,6 +56,7 @@
"devDependencies": { "devDependencies": {
"@gib/prettier-config": "workspace:", "@gib/prettier-config": "workspace:",
"@turbo/gen": "^2.7.3", "@turbo/gen": "^2.7.3",
"baseline-browser-mapping": "^2.9.14",
"dotenv-cli": "^10.0.0", "dotenv-cli": "^10.0.0",
"prettier": "catalog:", "prettier": "catalog:",
"turbo": "^2.7.3", "turbo": "^2.7.3",

View File

@@ -8,15 +8,18 @@
* @module * @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 utils from "../utils.js";
import type { import type {
ApiFromModules, ApiFromModules,
FilterApi, FilterApi,
FunctionReference, FunctionReference,
} from 'convex/server'; } from "convex/server";
import type * as notes from '../notes.js';
import type * as openai from '../openai.js';
import type * as utils from '../utils.js';
/** /**
* A utility for referencing Convex functions in your app's API. * A utility for referencing Convex functions in your app's API.
@@ -27,15 +30,22 @@ import type * as utils from '../utils.js';
* ``` * ```
*/ */
declare const fullApi: ApiFromModules<{ declare const fullApi: ApiFromModules<{
notes: typeof notes; auth: typeof auth;
openai: typeof openai; "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;
utils: typeof utils; utils: typeof utils;
}>; }>;
declare const fullApiWithMounts: typeof fullApi;
export declare const api: FilterApi< export declare const api: FilterApi<
typeof fullApi, typeof fullApiWithMounts,
FunctionReference<any, 'public'> FunctionReference<any, "public">
>; >;
export declare const internal: FilterApi< export declare const internal: FilterApi<
typeof fullApi, typeof fullApiWithMounts,
FunctionReference<any, 'internal'> FunctionReference<any, "internal">
>; >;
export declare const components: {};

View File

@@ -8,7 +8,7 @@
* @module * @module
*/ */
import { anyApi } from 'convex/server'; import { anyApi, componentsGeneric } from "convex/server";
/** /**
* A utility for referencing Convex functions in your app's API. * 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 api = anyApi;
export const internal = anyApi; export const internal = anyApi;
export const components = componentsGeneric();

View File

@@ -11,12 +11,11 @@
import type { import type {
DataModelFromSchemaDefinition, DataModelFromSchemaDefinition,
DocumentByName, DocumentByName,
SystemTableNames,
TableNamesInDataModel, TableNamesInDataModel,
} from 'convex/server'; SystemTableNames,
import type { GenericId } from 'convex/values'; } from "convex/server";
import type { GenericId } from "convex/values";
import schema from '../schema.js'; import schema from "../schema.js";
/** /**
* The names of all of your Convex tables. * The names of all of your Convex tables.

View File

@@ -10,17 +10,23 @@
import { import {
ActionBuilder, ActionBuilder,
GenericActionCtx, AnyComponents,
GenericDatabaseReader,
GenericDatabaseWriter,
GenericMutationCtx,
GenericQueryCtx,
HttpActionBuilder, HttpActionBuilder,
MutationBuilder, MutationBuilder,
QueryBuilder, QueryBuilder,
} from 'convex/server'; GenericActionCtx,
GenericMutationCtx,
GenericQueryCtx,
GenericDatabaseReader,
GenericDatabaseWriter,
FunctionReference,
} from "convex/server";
import type { DataModel } from "./dataModel.js";
import type { DataModel } from './dataModel.js'; type GenericCtx =
| GenericActionCtx<DataModel>
| GenericMutationCtx<DataModel>
| GenericQueryCtx<DataModel>;
/** /**
* Define a query in this Convex app's public API. * Define a query in this Convex app's public API.
@@ -30,7 +36,7 @@ import type { DataModel } from './dataModel.js';
* @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible. * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/ */
export declare const query: QueryBuilder<DataModel, 'public'>; export declare const query: QueryBuilder<DataModel, "public">;
/** /**
* Define a query that is only accessible from other Convex functions (but not from the client). * Define a query that is only accessible from other Convex functions (but not from the client).
@@ -40,7 +46,7 @@ export declare const query: QueryBuilder<DataModel, 'public'>;
* @param func - The query function. It receives a {@link QueryCtx} as its first argument. * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
* @returns The wrapped query. Include this as an `export` to name it and make it accessible. * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
*/ */
export declare const internalQuery: QueryBuilder<DataModel, 'internal'>; export declare const internalQuery: QueryBuilder<DataModel, "internal">;
/** /**
* Define a mutation in this Convex app's public API. * Define a mutation in this Convex app's public API.
@@ -50,7 +56,7 @@ export declare const internalQuery: QueryBuilder<DataModel, 'internal'>;
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/ */
export declare const mutation: MutationBuilder<DataModel, 'public'>; export declare const mutation: MutationBuilder<DataModel, "public">;
/** /**
* Define a mutation that is only accessible from other Convex functions (but not from the client). * Define a mutation that is only accessible from other Convex functions (but not from the client).
@@ -60,7 +66,7 @@ export declare const mutation: MutationBuilder<DataModel, 'public'>;
* @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
* @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
*/ */
export declare const internalMutation: MutationBuilder<DataModel, 'internal'>; export declare const internalMutation: MutationBuilder<DataModel, "internal">;
/** /**
* Define an action in this Convex app's public API. * Define an action in this Convex app's public API.
@@ -73,7 +79,7 @@ export declare const internalMutation: MutationBuilder<DataModel, 'internal'>;
* @param func - The action. It receives an {@link ActionCtx} as its first argument. * @param func - The action. It receives an {@link ActionCtx} as its first argument.
* @returns The wrapped action. Include this as an `export` to name it and make it accessible. * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
*/ */
export declare const action: ActionBuilder<DataModel, 'public'>; export declare const action: ActionBuilder<DataModel, "public">;
/** /**
* Define an action that is only accessible from other Convex functions (but not from the client). * Define an action that is only accessible from other Convex functions (but not from the client).
@@ -81,7 +87,7 @@ export declare const action: ActionBuilder<DataModel, 'public'>;
* @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.
* @returns The wrapped function. Include this as an `export` to name it and make it accessible. * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
*/ */
export declare const internalAction: ActionBuilder<DataModel, 'internal'>; export declare const internalAction: ActionBuilder<DataModel, "internal">;
/** /**
* Define an HTTP action. * Define an HTTP action.

View File

@@ -11,12 +11,13 @@
import { import {
actionGeneric, actionGeneric,
httpActionGeneric, httpActionGeneric,
queryGeneric,
mutationGeneric,
internalActionGeneric, internalActionGeneric,
internalMutationGeneric, internalMutationGeneric,
internalQueryGeneric, internalQueryGeneric,
mutationGeneric, componentsGeneric,
queryGeneric, } from "convex/server";
} from 'convex/server';
/** /**
* Define a query in this Convex app's public API. * Define a query in this Convex app's public API.

View File

@@ -6,15 +6,18 @@
"description": "Convex Backend for Monorepo", "description": "Convex Backend for Monorepo",
"author": "Gib", "author": "Gib",
"license": "MIT", "license": "MIT",
"exports": {}, "exports": {
"./types" : "./types/index.ts"
},
"scripts": { "scripts": {
"dev": "convex dev", "dev": "bun with-env convex dev",
"dev:tunnel": "convex dev", "dev:tunnel": "bun with-env convex dev",
"setup": "convex dev --until-success", "setup": "bun with-env convex dev --until-success",
"clean": "git clean -xdf .cache .turbo dist node_modules", "clean": "git clean -xdf .cache .turbo dist node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore", "format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint --flag unstable_native_nodejs_ts_config", "lint": "eslint --flag unstable_native_nodejs_ts_config",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env --"
}, },
"dependencies": { "dependencies": {
"@oslojs/crypto": "^1.0.1", "@oslojs/crypto": "^1.0.1",

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env node
import { exportJWK, exportPKCS8, generateKeyPair } from "jose";
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 }] });
process.stdout.write(
`JWT_PRIVATE_KEY="${privateKey.trimEnd().replace(/\n/g, " ")}"`,
);
process.stdout.write("\n");
process.stdout.write(`JWKS=${jwks}`);
process.stdout.write("\n");

View File

@@ -0,0 +1,4 @@
export const PASSWORD_MIN = 8;
export const PASSWORD_MAX = 100;
export const PASSWORD_REGEX =
/^(?=.{8,100}$)(?!.*\s)(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\p{P}\p{S}]).*$/u;

View File

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