diff --git a/src/app/(admin)/dashboard/_components/passkey.tsx b/src/app/(admin)/dashboard/_components/passkey.tsx
new file mode 100644
index 0000000..d98ca06
--- /dev/null
+++ b/src/app/(admin)/dashboard/_components/passkey.tsx
@@ -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 (
+
+
+ )
+}
+
+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 (
+
+
+
+
+
+
+ {name}
+
+
+ Created at: {new Date(createdAt).toLocaleString()}
+
+
+
+
+ )
+}
diff --git a/src/app/(admin)/dashboard/_components/passkeys-list.tsx b/src/app/(admin)/dashboard/_components/passkeys-list.tsx
new file mode 100644
index 0000000..92d6d51
--- /dev/null
+++ b/src/app/(admin)/dashboard/_components/passkeys-list.tsx
@@ -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 (
+
+ {passkeys.length === 0 ?
No passkeys registered.
: (
+
+ {passkeys.map(passkey => (
+
+ ))}
+
+ )}
+
+ )
+}
diff --git a/src/app/(admin)/dashboard/_components/sidebar-user-dropdown.tsx b/src/app/(admin)/dashboard/_components/sidebar-user-dropdown.tsx
index e74cf60..745c117 100644
--- a/src/app/(admin)/dashboard/_components/sidebar-user-dropdown.tsx
+++ b/src/app/(admin)/dashboard/_components/sidebar-user-dropdown.tsx
@@ -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({
- fetchOptions: {
- onSuccess: () => {
- router.push("/")
- }
+ 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)
}
}
diff --git a/src/app/(admin)/dashboard/_components/sidebar.tsx b/src/app/(admin)/dashboard/_components/sidebar.tsx
index 05fd3d5..71fb1c2 100644
--- a/src/app/(admin)/dashboard/_components/sidebar.tsx
+++ b/src/app/(admin)/dashboard/_components/sidebar.tsx
@@ -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() {
+
+
+ Menu
+
+
+
+
+
+
+
+ Home
+
+
+
+
+
+
Navigation
@@ -81,6 +98,27 @@ export function DashboardSidebar() {
+
+
+ Other
+
+
+
+
+
+
+
+ User Profile
+
+
+
+
+
+
diff --git a/src/app/(admin)/dashboard/user/page.tsx b/src/app/(admin)/dashboard/user/page.tsx
new file mode 100644
index 0000000..98f8cc2
--- /dev/null
+++ b/src/app/(admin)/dashboard/user/page.tsx
@@ -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 (
+
+
+
User Profile
+ Manage user settings
+
+
+
+
+ )
+}
diff --git a/src/app/sign-in/_components/signin-button.tsx b/src/app/sign-in/_components/signin-button.tsx
index c4025b3..f0d16d4 100644
--- a/src/app/sign-in/_components/signin-button.tsx
+++ b/src/app/sign-in/_components/signin-button.tsx
@@ -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,40 +16,85 @@ export function OAuthSignInButton({
className
}: OAuthSignInButtonProps) {
const [isLoading, setIsLoading] = useState(false)
+ const router = useRouter()
- const handleSignIn = async () => {
- try {
- setIsLoading(true)
- await signIn.oauth2({
- providerId: "authentik",
- callbackURL: "/dashboard"
- })
- } catch (error) {
- console.error("Sign in failed:", error)
+ const handleOAuthSignIn = async () => {
+ setIsLoading(true)
+ const res = await signIn.oauth2({
+ providerId: "authentik",
+ callbackURL: "/dashboard"
+ })
+
+ 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 (
-
+ <>
+
+
+ >
)
}
diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx
new file mode 100644
index 0000000..f7bc035
--- /dev/null
+++ b/src/components/ui/dialog.tsx
@@ -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) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger }
diff --git a/src/components/user-dropdown.tsx b/src/components/user-dropdown.tsx
index e3ffbc9..cbc10ac 100644
--- a/src/components/user-dropdown.tsx
+++ b/src/components/user-dropdown.tsx
@@ -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({
- fetchOptions: {
- onSuccess: () => {
- router.push("/")
- }
+ 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
}
}
diff --git a/src/lib/auth/auth-client.ts b/src/lib/auth/auth-client.ts
index ea11537..c2b891e 100644
--- a/src/lib/auth/auth-client.ts
+++ b/src/lib/auth/auth-client.ts
@@ -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()]
})
diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts
index 2c334f5..54ee7af 100644
--- a/src/lib/auth/auth.ts
+++ b/src/lib/auth/auth.ts
@@ -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()
]
})
diff --git a/src/lib/dashboard/stats.ts b/src/lib/dashboard/stats.ts
index 585f98c..a382dce 100644
--- a/src/lib/dashboard/stats.ts
+++ b/src/lib/dashboard/stats.ts
@@ -3,33 +3,24 @@ 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
- .select({
- id: urls.id,
- title: urls.title,
- slug: urls.slug,
- visitCount: count(visits.id)
- })
- .from(urls)
- .leftJoin(visits, eq(urls.id, visits.urlId))
- .groupBy(urls.id, urls.title, urls.slug)
- .orderBy(desc(count(visits.id)))
- .limit(1)
+ const [urlsCount] = await db.select({ count: count() }).from(urls)
+ const [visitsCount] = await db.select({ count: count() }).from(visits)
+ const mostVisitedUrl = await db
+ .select({
+ id: urls.id,
+ title: urls.title,
+ slug: urls.slug,
+ visitCount: count(visits.id)
+ })
+ .from(urls)
+ .leftJoin(visits, eq(urls.id, visits.urlId))
+ .groupBy(urls.id, urls.title, urls.slug)
+ .orderBy(desc(count(visits.id)))
+ .limit(1)
- return {
- totalUrls: urlsCount.count,
- totalVisits: visitsCount.count,
- mostVisitedUrl: mostVisitedUrl[0] || null
- }
- } catch (error) {
- console.error("Failed to fetch dashboard stats:", error)
- return {
- totalUrls: 0,
- totalVisits: 0,
- mostVisitedUrl: null
- }
+ return {
+ totalUrls: urlsCount.count,
+ totalVisits: visitsCount.count,
+ mostVisitedUrl: mostVisitedUrl[0] || null
}
}
diff --git a/src/lib/drizzle/auth-schema.ts b/src/lib/drizzle/auth-schema.ts
index 5fca4e0..f6d540d 100644
--- a/src/lib/drizzle/auth-schema.ts
+++ b/src/lib/drizzle/auth-schema.ts
@@ -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")
+})
diff --git a/src/lib/drizzle/migrations/0001_jazzy_meltdown.sql b/src/lib/drizzle/migrations/0001_jazzy_meltdown.sql
new file mode 100644
index 0000000..fdd1c82
--- /dev/null
+++ b/src/lib/drizzle/migrations/0001_jazzy_meltdown.sql
@@ -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;
\ No newline at end of file
diff --git a/src/lib/drizzle/migrations/meta/0001_snapshot.json b/src/lib/drizzle/migrations/meta/0001_snapshot.json
new file mode 100644
index 0000000..f3693f5
--- /dev/null
+++ b/src/lib/drizzle/migrations/meta/0001_snapshot.json
@@ -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": {}
+ }
+}
\ No newline at end of file
diff --git a/src/lib/drizzle/migrations/meta/_journal.json b/src/lib/drizzle/migrations/meta/_journal.json
index 52c6132..74c7de3 100644
--- a/src/lib/drizzle/migrations/meta/_journal.json
+++ b/src/lib/drizzle/migrations/meta/_journal.json
@@ -8,6 +8,13 @@
"when": 1751119888991,
"tag": "0000_shallow_stryfe",
"breakpoints": true
+ },
+ {
+ "idx": 1,
+ "version": "7",
+ "when": 1754582718320,
+ "tag": "0001_jazzy_meltdown",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/src/lib/drizzle/schema.ts b/src/lib/drizzle/schema.ts
index 6b1f37c..42329e2 100644
--- a/src/lib/drizzle/schema.ts
+++ b/src/lib/drizzle/schema.ts
@@ -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"
diff --git a/tsconfig.json b/tsconfig.json
index 06ee0db..9dc5814 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -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"
+ ]
}