Added passkey support
This commit is contained in:
111
src/app/(admin)/dashboard/_components/passkey.tsx
Normal file
111
src/app/(admin)/dashboard/_components/passkey.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { passkey } from "@/lib/auth/auth-client"
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { KeyRoundIcon } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { toast } from "sonner"
|
||||
import { z } from "zod"
|
||||
|
||||
export function PasskeyAdd() {
|
||||
const router = useRouter()
|
||||
const form = useForm<{ name: string }>({
|
||||
resolver: zodResolver(z.object({ name: z.string().min(1).max(50) })),
|
||||
defaultValues: { name: "passkey" }
|
||||
})
|
||||
|
||||
async function handleSubmit({ name }: { name: string }) {
|
||||
const res = await passkey.addPasskey({ name })
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Passkey added successfully")
|
||||
router.refresh()
|
||||
form.reset({ name: "" })
|
||||
}
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4 mb-5 max-w-48">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Name</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit" disabled={form.formState.isSubmitting}>
|
||||
{form.formState.isSubmitting ? "Adding..." : "Add Passkey"}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export function PasskeyItem({ id, name, createdAt }: { id: string, name?: string, createdAt: Date }) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
async function handleDelete() {
|
||||
setIsLoading(true)
|
||||
await passkey.deletePasskey({ id })
|
||||
router.refresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mb-2">
|
||||
<CardContent>
|
||||
<p className="font-medium flex gap-2">
|
||||
<span>
|
||||
<KeyRoundIcon />
|
||||
</span>
|
||||
<span>{name}</span>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Created at: {new Date(createdAt).toLocaleString()}
|
||||
</p>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild className="mt-2">
|
||||
<Button variant="destructive">
|
||||
Delete
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Are you sure?</DialogTitle>
|
||||
<DialogDescription>
|
||||
This action cannot be undone. This will permanently delete the passkey.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<DialogClose asChild>
|
||||
<Button variant="outline">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogClose>
|
||||
<Button className="ml-2" variant="destructive" disabled={isLoading} onClick={handleDelete}>
|
||||
{isLoading ? "Deleting..." : "Delete"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
26
src/app/(admin)/dashboard/_components/passkeys-list.tsx
Normal file
26
src/app/(admin)/dashboard/_components/passkeys-list.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { auth } from "@/lib/auth/auth"
|
||||
import { headers } from "next/headers"
|
||||
import { PasskeyItem } from "./passkey"
|
||||
|
||||
export default async function PasskeysList() {
|
||||
const passkeys = await auth.api.listPasskeys({
|
||||
headers: await headers()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
{passkeys.length === 0 ? <p className="text-sm text-muted-foreground">No passkeys registered.</p> : (
|
||||
<div className="max-w-md">
|
||||
{passkeys.map(passkey => (
|
||||
<PasskeyItem
|
||||
key={passkey.id}
|
||||
id={passkey.id}
|
||||
name={passkey.name}
|
||||
createdAt={passkey.createdAt}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { signOut, useSession } from "@/lib/auth/auth-client"
|
||||
import { LogOut, User } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
|
||||
export function SidebarUserDropdown() {
|
||||
const { data: session, isPending } = useSession()
|
||||
@@ -21,16 +22,16 @@ export function SidebarUserDropdown() {
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await signOut({
|
||||
const res = await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/")
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Failed to sign out:", err)
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { LayoutDashboard, List, PanelLeft, Plus } from "lucide-react"
|
||||
import { HomeIcon, LayoutDashboard, List, PanelLeft, Plus, User2 } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
@@ -58,6 +58,23 @@ export function DashboardSidebar() {
|
||||
</SidebarMenu>
|
||||
</SidebarHeader>
|
||||
<SidebarContent>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
Menu
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton asChild tooltip="Home">
|
||||
<Link href="/">
|
||||
<HomeIcon />
|
||||
<span>Home</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel className="group-data-[collapsible=icon]:hidden">
|
||||
Navigation
|
||||
@@ -81,6 +98,27 @@ export function DashboardSidebar() {
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
<SidebarGroup>
|
||||
<SidebarGroupLabel>
|
||||
Other
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu>
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={pathname === "/dashboard/user"}
|
||||
tooltip="User Profile"
|
||||
>
|
||||
<Link href="/dashboard/user">
|
||||
<User2 />
|
||||
<span>User Profile</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
</SidebarMenu>
|
||||
</SidebarGroupContent>
|
||||
</SidebarGroup>
|
||||
</SidebarContent>
|
||||
<SidebarFooter>
|
||||
<SidebarThemeToggle />
|
||||
|
||||
22
src/app/(admin)/dashboard/user/page.tsx
Normal file
22
src/app/(admin)/dashboard/user/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import { getSession } from "@/lib/auth/session"
|
||||
import { PasskeyAdd } from "../_components/passkey"
|
||||
import PasskeysList from "../_components/passkeys-list"
|
||||
|
||||
export default async function UserPage() {
|
||||
const { session, redirect } = await getSession()
|
||||
|
||||
if (!session) {
|
||||
redirect("/login")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="mb-6">
|
||||
<h1 className="block mb-2 text-2xl font-bold text-foreground">User Profile</h1>
|
||||
<h1 className="block text-muted-foreground">Manage user settings</h1>
|
||||
</div>
|
||||
<PasskeyAdd />
|
||||
<PasskeysList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
import { AuthentikIcon } from "@/components/svgs/authentik"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { signIn } from "@/lib/auth/auth-client"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { KeyIcon, Loader2 } from "lucide-react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
interface OAuthSignInButtonProps {
|
||||
className?: string
|
||||
@@ -14,23 +16,47 @@ export function OAuthSignInButton({
|
||||
className
|
||||
}: OAuthSignInButtonProps) {
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignIn = async () => {
|
||||
try {
|
||||
const handleOAuthSignIn = async () => {
|
||||
setIsLoading(true)
|
||||
await signIn.oauth2({
|
||||
const res = await signIn.oauth2({
|
||||
providerId: "authentik",
|
||||
callbackURL: "/dashboard"
|
||||
})
|
||||
} catch (error) {
|
||||
console.error("Sign in failed:", error)
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Successfully logged in!")
|
||||
}
|
||||
|
||||
const handlePasskeySignIn = async () => {
|
||||
setIsLoading(true)
|
||||
const res = await signIn.passkey({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/dashboard")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
toast.success("Successfully logged in!")
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleSignIn}
|
||||
onClick={handleOAuthSignIn}
|
||||
disabled={isLoading}
|
||||
className={className}
|
||||
size="lg"
|
||||
@@ -49,5 +75,26 @@ export function OAuthSignInButton({
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handlePasskeySignIn}
|
||||
disabled={isLoading}
|
||||
className={className}
|
||||
size="lg"
|
||||
>
|
||||
{isLoading ?
|
||||
(
|
||||
<>
|
||||
<Loader2 className="mr-2 w-4 h-4 animate-spin" />
|
||||
Signing in...
|
||||
</>
|
||||
) :
|
||||
(
|
||||
<>
|
||||
<KeyIcon className="mr-2 w-5 h-5" />
|
||||
Sign in with Passkey
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
132
src/components/ui/dialog.tsx
Normal file
132
src/components/ui/dialog.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
"use client"
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
|
||||
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
|
||||
}
|
||||
|
||||
function DialogPortal({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
|
||||
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
|
||||
}
|
||||
|
||||
function DialogClose({
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
|
||||
return (
|
||||
<DialogPrimitive.Overlay
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
showCloseButton = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
<DialogOverlay />
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close
|
||||
data-slot="dialog-close"
|
||||
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
|
||||
>
|
||||
<XIcon />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogTitle({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn("text-lg leading-none font-semibold", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn("text-muted-foreground text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }
|
||||
@@ -14,6 +14,7 @@ import { signOut, useSession } from "@/lib/auth/auth-client"
|
||||
import { LogOut, Settings } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { toast } from "sonner"
|
||||
|
||||
interface UserDropdownProps {
|
||||
className?: string
|
||||
@@ -24,16 +25,17 @@ export function UserDropdown({ className }: UserDropdownProps) {
|
||||
const router = useRouter()
|
||||
|
||||
const handleSignOut = async () => {
|
||||
try {
|
||||
await signOut({
|
||||
const res = await signOut({
|
||||
fetchOptions: {
|
||||
onSuccess: () => {
|
||||
router.push("/")
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error("Failed to sign out:", err)
|
||||
|
||||
if (res && res.error) {
|
||||
toast.error(res.error.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { genericOAuthClient } from "better-auth/client/plugins"
|
||||
import { genericOAuthClient, passkeyClient } from "better-auth/client/plugins"
|
||||
import { createAuthClient } from "better-auth/react"
|
||||
|
||||
export const { signIn, signOut, useSession } = createAuthClient({
|
||||
plugins: [genericOAuthClient()]
|
||||
export const { signIn, signOut, useSession, passkey } = createAuthClient({
|
||||
plugins: [genericOAuthClient(), passkeyClient()]
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { betterAuth } from "better-auth"
|
||||
import { drizzleAdapter } from "better-auth/adapters/drizzle"
|
||||
import { nextCookies } from "better-auth/next-js"
|
||||
import { genericOAuth } from "better-auth/plugins"
|
||||
import { passkey } from "better-auth/plugins/passkey"
|
||||
import { db } from "../drizzle/db"
|
||||
import { env } from "../env/server"
|
||||
import { getGravatar } from "../gravatar"
|
||||
@@ -37,6 +38,7 @@ export const auth = betterAuth({
|
||||
clientSecret: env.AUTHENTIK_CLIENT_SECRET,
|
||||
discoveryUrl: env.AUTHENTIK_DISCOVERY_URL
|
||||
}]
|
||||
})
|
||||
}),
|
||||
passkey()
|
||||
]
|
||||
})
|
||||
|
||||
@@ -3,7 +3,6 @@ import { db } from "../drizzle/db"
|
||||
import { urls, visits } from "../drizzle/schema"
|
||||
|
||||
export async function getDashboardStats() {
|
||||
try {
|
||||
const [urlsCount] = await db.select({ count: count() }).from(urls)
|
||||
const [visitsCount] = await db.select({ count: count() }).from(visits)
|
||||
const mostVisitedUrl = await db
|
||||
@@ -24,12 +23,4 @@ export async function getDashboardStats() {
|
||||
totalVisits: visitsCount.count,
|
||||
mostVisitedUrl: mostVisitedUrl[0] || null
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch dashboard stats:", error)
|
||||
return {
|
||||
totalUrls: 0,
|
||||
totalVisits: 0,
|
||||
mostVisitedUrl: null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { boolean, pgTable, text, timestamp } from "drizzle-orm/pg-core"
|
||||
import { boolean, integer, pgTable, text, timestamp } from "drizzle-orm/pg-core"
|
||||
|
||||
export const user = pgTable("user", {
|
||||
id: text("id").primaryKey(),
|
||||
@@ -45,3 +45,17 @@ export const verification = pgTable("verification", {
|
||||
createdAt: timestamp("created_at").$defaultFn(() => new Date()),
|
||||
updatedAt: timestamp("updated_at").$defaultFn(() => new Date())
|
||||
})
|
||||
|
||||
export const passkey = pgTable("passkey", {
|
||||
id: text("id").primaryKey(),
|
||||
name: text("name"),
|
||||
publicKey: text("public_key").notNull(),
|
||||
userId: text("user_id").notNull().references(() => user.id, { onDelete: "cascade" }),
|
||||
credentialID: text("credential_i_d").notNull(),
|
||||
counter: integer("counter").notNull(),
|
||||
deviceType: text("device_type").notNull(),
|
||||
backedUp: boolean("backed_up").notNull(),
|
||||
transports: text("transports"),
|
||||
createdAt: timestamp("created_at"),
|
||||
aaguid: text("aaguid")
|
||||
})
|
||||
|
||||
15
src/lib/drizzle/migrations/0001_jazzy_meltdown.sql
Normal file
15
src/lib/drizzle/migrations/0001_jazzy_meltdown.sql
Normal file
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE "passkey" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text,
|
||||
"public_key" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"credential_i_d" text NOT NULL,
|
||||
"counter" integer NOT NULL,
|
||||
"device_type" text NOT NULL,
|
||||
"backed_up" boolean NOT NULL,
|
||||
"transports" text,
|
||||
"created_at" timestamp,
|
||||
"aaguid" text
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "passkey" ADD CONSTRAINT "passkey_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
626
src/lib/drizzle/migrations/meta/0001_snapshot.json
Normal file
626
src/lib/drizzle/migrations/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,626 @@
|
||||
{
|
||||
"id": "4f8ffa1f-1feb-4438-8743-722c9a6b601e",
|
||||
"prevId": "87caaf54-7746-4c06-974e-384f1111759c",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.passkey": {
|
||||
"name": "passkey",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"public_key": {
|
||||
"name": "public_key",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"credential_i_d": {
|
||||
"name": "credential_i_d",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"counter": {
|
||||
"name": "counter",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"device_type": {
|
||||
"name": "device_type",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"backed_up": {
|
||||
"name": "backed_up",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"transports": {
|
||||
"name": "transports",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"aaguid": {
|
||||
"name": "aaguid",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"passkey_user_id_user_id_fk": {
|
||||
"name": "passkey_user_id_user_id_fk",
|
||||
"tableFrom": "passkey",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.urls": {
|
||||
"name": "urls",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"url": {
|
||||
"name": "url",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"slug": {
|
||||
"name": "slug",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"title": {
|
||||
"name": "title",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"max_visits": {
|
||||
"name": "max_visits",
|
||||
"type": "integer",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"exp_date": {
|
||||
"name": "exp_date",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"forward_query_params": {
|
||||
"name": "forward_query_params",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": true
|
||||
},
|
||||
"crawable": {
|
||||
"name": "crawable",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"urls_slug_idx": {
|
||||
"name": "urls_slug_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "slug",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"urls_url_idx": {
|
||||
"name": "urls_url_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "url",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
},
|
||||
"exp_date_idx": {
|
||||
"name": "exp_date_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "exp_date",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"urls_slug_unique": {
|
||||
"name": "urls_slug_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"slug"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.visits": {
|
||||
"name": "visits",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "uuid",
|
||||
"primaryKey": true,
|
||||
"notNull": true,
|
||||
"default": "gen_random_uuid()"
|
||||
},
|
||||
"url_id": {
|
||||
"name": "url_id",
|
||||
"type": "uuid",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "varchar",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp with time zone",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {
|
||||
"visits_url_id_idx": {
|
||||
"name": "visits_url_id_idx",
|
||||
"columns": [
|
||||
{
|
||||
"expression": "url_id",
|
||||
"isExpression": false,
|
||||
"asc": true,
|
||||
"nulls": "last"
|
||||
}
|
||||
],
|
||||
"isUnique": false,
|
||||
"concurrently": false,
|
||||
"method": "btree",
|
||||
"with": {}
|
||||
}
|
||||
},
|
||||
"foreignKeys": {
|
||||
"visits_url_id_urls_id_fk": {
|
||||
"name": "visits_url_id_urls_id_fk",
|
||||
"tableFrom": "visits",
|
||||
"tableTo": "urls",
|
||||
"columnsFrom": [
|
||||
"url_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "no action",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,13 @@
|
||||
"when": 1751119888991,
|
||||
"tag": "0000_shallow_stryfe",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1754582718320,
|
||||
"tag": "0001_jazzy_meltdown",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
export { account, session, user, verification } from "./auth-schema"
|
||||
export * from "./auth-schema"
|
||||
|
||||
import { relations } from "drizzle-orm"
|
||||
import { boolean, index, integer, pgTable, timestamp, uuid, varchar } from "drizzle-orm/pg-core"
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -19,9 +23,18 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user