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 ( +
+ + ( + + Name + + + + + + )} + /> + + + + ) +} + +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()} +

+ + + + + + + Are you sure? + + This action cannot be undone. This will permanently delete the passkey. + + + + + + + + + + +
+
+ ) +} 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" + ] }