proper user/session creation and auth integration into UI

This commit is contained in:
2025-08-19 03:40:06 -04:00
parent 1a13013b45
commit 12e9ec5d85
12 changed files with 726 additions and 94 deletions

View File

@@ -1,6 +1,16 @@
import { NextResponse } from "next/server";
import { auth } from "@/auth";
export async function POST(request: Request) {
const session = await auth();
if (!session?.user) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 }
);
}
const { prompt } = await request.json();
const systemPrompt = `

View File

@@ -1,37 +1,43 @@
import { signIn, signOut } from "@/auth"
import { auth } from "@/auth"
import { Button } from "@/components/ui/button"
"use client"
export default async function SignIn() {
const session = await auth()
import { signOut, useSession } from "next-auth/react"
import { Button } from "@/components/ui/button"
import { useRouter } from "next/navigation"
export default function SignIn() {
const { data: session, status } = useSession()
const router = useRouter()
const handleSignOut = async () => {
await signOut({ redirect: false })
router.push("/")
router.refresh()
}
if (status === "loading") {
return <div className="h-8 w-16 bg-muted animate-pulse rounded"></div>
}
if (session?.user) {
return (
<div className="flex items-center gap-4">
<form
action={async () => {
"use server"
await signOut()
}}
>
<Button type="submit" variant="secondary" >
Sign Out
</Button>
</form>
<span className="text-sm text-muted-foreground hidden sm:inline">
{session.user.name || session.user.email}
</span>
<Button onClick={handleSignOut} variant="ghost" size="sm">
Sign Out
</Button>
</div>
)
}
return (
<form
action={async () => {
"use server"
await signIn("authentik")
}}
<Button
onClick={() => router.push("/signin")}
variant="outline"
size="sm"
>
<Button type="submit" variant="default" >
Sign In
</Button>
</form>
Sign In
</Button>
)
}

View File

@@ -143,6 +143,13 @@ export default function HomePage() {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ prompt: aiPrompt })
})
if (res.status === 401) {
alert('Please sign in to use AI features.')
setAiLoading(false)
return
}
const data = await res.json()
if (Array.isArray(data) && data.length > 0) {
@@ -225,25 +232,32 @@ export default function HomePage() {
<div className='max-w-fit m-auto'> Drag & Drop *.ics here</div>
</div>
{/* AI Toolbar */}
<div className="flex flex-row gap-4 mb-4 items-start">
{session?.user && (
<>
<div className='w-full'>
<Textarea
className="wrap-anywhere min-h-12"
placeholder='Describe event for AI to create'
value={aiPrompt}
onChange={e => setAiPrompt(e.target.value)}
/>
</div>
<div className='flex flex-row gap-2 pt-1.5'>
<Button onClick={handleAiCreate} disabled={aiLoading}>
{aiLoading ? 'Thinking...' : 'AI Create'}
</Button>
</div>
</>
)}
</div>
{session?.user ? (
<div className="flex flex-row gap-4 mb-4 items-start">
<div className='w-full'>
<Textarea
className="wrap-anywhere min-h-12"
placeholder='Describe event for AI to create'
value={aiPrompt}
onChange={e => setAiPrompt(e.target.value)}
/>
</div>
<div className='flex flex-row gap-2 pt-1.5'>
<Button onClick={handleAiCreate} disabled={aiLoading}>
{aiLoading ? 'Thinking...' : 'AI Create'}
</Button>
</div>
</div>
) : (
<div className="mb-4 p-4 border border-dashed rounded-lg text-center">
<div className="text-sm text-muted-foreground mb-2">
Sign in to unlock AI-powered calendar features
</div>
<Button variant="outline" size="sm" asChild>
<a href="/signin">Sign In</a>
</Button>
</div>
)}
{/* Summary Panel */}
{

45
src/app/signin/page.tsx Normal file
View File

@@ -0,0 +1,45 @@
import { signIn, auth } from "@/auth"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { redirect } from "next/navigation"
import Link from "next/link"
export default async function SignInPage() {
const session = await auth()
// If already signed in, redirect to home
if (session?.user) {
redirect("/")
}
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Welcome</CardTitle>
<CardDescription>
Sign in to access AI-powered calendar features
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<form
action={async () => {
"use server"
await signIn("authentik", { redirectTo: "/" })
}}
>
<Button type="submit" className="w-full" size="lg">
Continue with Authentik
</Button>
</form>
<div className="text-center">
<Link href="/" className="text-sm text-muted-foreground hover:underline">
Continue without signing in
</Link>
</div>
</CardContent>
</Card>
</div>
)
}

49
src/app/signout/page.tsx Normal file
View File

@@ -0,0 +1,49 @@
import { signOut, auth } from "@/auth"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { redirect } from "next/navigation"
import Link from "next/link"
export default async function SignOutPage() {
const session = await auth()
if (!session) {
redirect("/")
}
return (
<div className="min-h-screen flex items-center justify-center bg-background p-4">
<Card className="w-full max-w-md">
<CardHeader className="text-center">
<CardTitle className="text-2xl font-bold">Sign Out</CardTitle>
<CardDescription>
Are you sure you want to sign out?
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="text-center p-3 bg-muted rounded-lg">
<div className="text-sm text-muted-foreground">Currently signed in as</div>
<div className="font-medium">{session.user?.name || session.user?.email}</div>
</div>
<div className="grid grid-cols-2 gap-3">
<form
action={async () => {
"use server"
await signOut({ redirectTo: "/" })
}}
>
<Button type="submit" variant="destructive" className="w-full">
Sign Out
</Button>
</form>
<Button variant="outline" asChild>
<Link href="/">Cancel</Link>
</Button>
</div>
</CardContent>
</Card>
</div>
)
}

View File

@@ -1,36 +1,55 @@
import { pgTable, text, timestamp, uuid } from 'drizzle-orm/pg-core';
import { pgTable, text, timestamp, integer, boolean, primaryKey } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: uuid('id').primaryKey().defaultRandom(),
email: text('email').notNull().unique(),
export const users = pgTable('user', {
id: text('id').primaryKey(),
name: text('name'),
email: text('email').notNull(),
emailVerified: timestamp('emailVerified', { mode: 'string' }),
image: text('image'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const accounts = pgTable('accounts', {
id: uuid('id').primaryKey().defaultRandom(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
export const accounts = pgTable('account', {
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
type: text('type').notNull(),
provider: text('provider').notNull(),
providerAccountId: text('provider_account_id').notNull(),
refreshToken: text('refresh_token'),
accessToken: text('access_token'),
expiresAt: timestamp('expires_at'),
tokenType: text('token_type'),
providerAccountId: text('providerAccountId').notNull(),
refresh_token: text('refresh_token'),
access_token: text('access_token'),
expires_at: text('expires_at'),
token_type: text('token_type'),
scope: text('scope'),
idToken: text('id_token'),
sessionState: text('session_state'),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
id_token: text('id_token'),
session_state: text('session_state'),
}, (account) => ({
compoundKey: primaryKey({ columns: [account.provider, account.providerAccountId] })
}));
export const sessions = pgTable('session', {
sessionToken: text().primaryKey().notNull(),
userId: text().notNull().references(() => users.id, { onDelete: 'cascade' }),
expires: timestamp({ mode: 'string' }).notNull(),
});
export const sessions = pgTable('sessions', {
id: uuid('id').primaryKey().defaultRandom(),
sessionToken: text('session_token').notNull().unique(),
userId: uuid('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
expires: timestamp('expires').notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export const verificationTokens = pgTable('verificationToken', {
identifier: text('identifier').notNull(),
token: text('token').notNull(),
expires: timestamp('expires', { mode: 'string' }).notNull(),
}, (vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] })
}));
export const authenticators = pgTable('authenticator', {
credentialID: text('credentialID').notNull().unique(),
userId: text('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
providerAccountId: text('providerAccountId').notNull(),
credentialPublicKey: text('credentialPublicKey').notNull(),
counter: integer('counter').notNull(),
credentialDeviceType: text('credentialDeviceType').notNull(),
credentialBackedUp: boolean('credentialBackedUp').notNull(),
transports: text('transports'),
}, (authenticator) => ({
compositePK: primaryKey({
columns: [authenticator.credentialID, authenticator.userId],
name: "authenticator_userId_credentialID_pk"
})
}));

View File

@@ -1,25 +0,0 @@
import { auth } from "@/auth"
export default auth((req) => {
const { nextUrl } = req
const isLoggedIn = !!req.auth
// Protect dashboard routes
// if (nextUrl.pathname.startsWith('/api') && !isLoggedIn) {
// return Response.redirect(new URL('/signin', nextUrl))
// }
// Redirect logged-in users from sign-in page
if (nextUrl.pathname.startsWith('/signin') && isLoggedIn) {
return Response.redirect(new URL('/', nextUrl))
}
})
export const config = {
matcher: [
// Skip Next.js internals and all static files
'/((?!_next|[^?]*\\.(?:html?|css|js(?!on)|jpe?g|webp|png|gif|svg|ttf|woff2?|ico|csv|docx?|xlsx?|zip|webmanifest)).*)',
// Always run for API routes
'/(api|trpc)(.*)',
],
}