chore: add .env.exmaple on examples
14
examples/nextjs-example/.env.exmaple
Normal file
@@ -0,0 +1,14 @@
|
||||
GOOGLE_CLIENT_SECRET=
|
||||
GOOGLE_CLIENT_ID=
|
||||
BETTER_AUTH_URL="http://localhost:3000"
|
||||
BETTER_AUTH_SECRET=
|
||||
TURSO_DATABASE_URL=
|
||||
TURSO_AUTH_TOKEN=
|
||||
GITHUB_CLIENT_ID=
|
||||
GITHUB_CLIENT_SECRET=
|
||||
RESEND_API_KEY=
|
||||
TEST_EMAIL=
|
||||
DISCORD_CLIENT_ID=
|
||||
DISCORD_CLIENT_SECRET=
|
||||
MICROSOFT_CLIENT_ID=
|
||||
MICROSOFT_CLIENT_SECRET=
|
||||
4
examples/nextjs-example/.gitignore
vendored
@@ -30,7 +30,7 @@ yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# env files (can opt-in for commiting if needed)
|
||||
.env*
|
||||
.env
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
@@ -38,3 +38,5 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
certificates
|
||||
@@ -1,19 +1,36 @@
|
||||
# Better Auth Next js example
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
This is an example of how to use Better Auth with Next.
|
||||
## Getting Started
|
||||
|
||||
**Implements the following features:**
|
||||
Email & Password . Social Sign-in . Passkeys . Email Verification . Password Reset . Two Factor Authentication . Profile Update . Session Management . Organization, Members and Roles
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
## How to run
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
1. Clone the code sandbox (or the repo) and open it in your code editor
|
||||
2. Move .env.example to .env and provide necessary variables
|
||||
3. Run the following commands
|
||||
```bash
|
||||
pnpm install
|
||||
pnpm dev
|
||||
```
|
||||
4. Open the browser and navigate to `http://localhost:3000`
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
|
||||
@@ -28,13 +28,11 @@ export default function Component() {
|
||||
setIsSubmitting(true);
|
||||
setError("");
|
||||
|
||||
// Simulate API call
|
||||
try {
|
||||
const res = await client.forgetPassword({
|
||||
email,
|
||||
redirectTo: "/reset-password",
|
||||
});
|
||||
// If the API call is successful, set isSubmitted to true
|
||||
setIsSubmitted(true);
|
||||
} catch (err) {
|
||||
setError("An error occurred. Please try again.");
|
||||
|
||||
@@ -18,16 +18,11 @@ import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function ResetPassword({
|
||||
params,
|
||||
}: {
|
||||
params: { token: string };
|
||||
}) {
|
||||
export default function ResetPassword() {
|
||||
const [password, setPassword] = useState("");
|
||||
const [confirmPassword, setConfirmPassword] = useState("");
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const token = params.token;
|
||||
const router = useRouter();
|
||||
async function handleSubmit(e: React.FormEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -32,7 +32,7 @@ export default function Component() {
|
||||
code: totpCode,
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.data?.status) {
|
||||
if (res.data?.session) {
|
||||
setSuccess(true);
|
||||
setError("");
|
||||
} else {
|
||||
|
||||
@@ -11,19 +11,15 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { CheckIcon, XIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { client, organization } from "@/lib/auth-client";
|
||||
import { InvitationError } from "./invitation-error";
|
||||
import { Invitation } from "@/lib/auth-types";
|
||||
|
||||
export default function InvitationPage({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
export default function InvitationPage() {
|
||||
const params = useParams<{
|
||||
id: string;
|
||||
};
|
||||
}) {
|
||||
}>();
|
||||
const router = useRouter();
|
||||
const [invitationStatus, setInvitationStatus] = useState<
|
||||
"pending" | "accepted" | "rejected"
|
||||
|
||||
460
examples/nextjs-example/app/admin/page.tsx
Normal file
@@ -0,0 +1,460 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { toast, Toaster } from "sonner";
|
||||
import { client } from "@/lib/auth-client";
|
||||
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import {
|
||||
Loader2,
|
||||
Plus,
|
||||
Trash,
|
||||
RefreshCw,
|
||||
UserCircle,
|
||||
Calendar as CalendarIcon,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Calendar } from "@/components/ui/calendar";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { format } from "date-fns";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
role: "admin" | "user";
|
||||
};
|
||||
|
||||
export default function AdminDashboard() {
|
||||
const queryClient = useQueryClient();
|
||||
const router = useRouter();
|
||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||
const [newUser, setNewUser] = useState({
|
||||
email: "",
|
||||
password: "",
|
||||
name: "",
|
||||
role: "user" as const,
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState<string | undefined>();
|
||||
const [isBanDialogOpen, setIsBanDialogOpen] = useState(false);
|
||||
const [banForm, setBanForm] = useState({
|
||||
userId: "",
|
||||
reason: "",
|
||||
expirationDate: undefined as Date | undefined,
|
||||
});
|
||||
|
||||
const { data: users, isLoading: isUsersLoading } = useQuery({
|
||||
queryKey: ["users"],
|
||||
queryFn: () =>
|
||||
client.admin
|
||||
.listUsers({
|
||||
query: {
|
||||
limit: 10,
|
||||
sortBy: "createdAt",
|
||||
sortDirection: "desc",
|
||||
},
|
||||
})
|
||||
.then((res) => res.data?.users ?? []),
|
||||
});
|
||||
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading("create");
|
||||
try {
|
||||
await client.admin.createUser({
|
||||
email: newUser.email,
|
||||
password: newUser.password,
|
||||
name: newUser.name,
|
||||
role: newUser.role,
|
||||
});
|
||||
toast.success("User created successfully");
|
||||
setNewUser({ email: "", password: "", name: "", role: "user" });
|
||||
setIsDialogOpen(false);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["users"],
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to create user");
|
||||
} finally {
|
||||
setIsLoading(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (id: string) => {
|
||||
setIsLoading(`delete-${id}`);
|
||||
try {
|
||||
await client.admin.removeUser({ userId: id });
|
||||
toast.success("User deleted successfully");
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["users"],
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to delete user");
|
||||
} finally {
|
||||
setIsLoading(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRevokeSessions = async (id: string) => {
|
||||
setIsLoading(`revoke-${id}`);
|
||||
try {
|
||||
await client.admin.revokeUserSessions({ userId: id });
|
||||
toast.success("Sessions revoked for user");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to revoke sessions");
|
||||
} finally {
|
||||
setIsLoading(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleImpersonateUser = async (id: string) => {
|
||||
setIsLoading(`impersonate-${id}`);
|
||||
try {
|
||||
await client.admin.impersonateUser({ userId: id });
|
||||
toast.success("Impersonated user");
|
||||
router.push("/dashboard");
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to impersonate user");
|
||||
} finally {
|
||||
setIsLoading(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const handleBanUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(`ban-${banForm.userId}`);
|
||||
try {
|
||||
if (!banForm.expirationDate) {
|
||||
throw new Error("Expiration date is required");
|
||||
}
|
||||
await client.admin.banUser({
|
||||
userId: banForm.userId,
|
||||
banReason: banForm.reason,
|
||||
banExpiresIn: banForm.expirationDate.getTime() - new Date().getTime(),
|
||||
});
|
||||
toast.success("User banned successfully");
|
||||
setIsBanDialogOpen(false);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["users"],
|
||||
});
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || "Failed to ban user");
|
||||
} finally {
|
||||
setIsLoading(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="container mx-auto p-4 space-y-8">
|
||||
<Toaster richColors />
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between">
|
||||
<CardTitle className="text-2xl">Admin Dashboard</CardTitle>
|
||||
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button>
|
||||
<Plus className="mr-2 h-4 w-4" /> Create User
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create New User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleCreateUser} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={newUser.email}
|
||||
onChange={(e) =>
|
||||
setNewUser({ ...newUser, email: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
value={newUser.password}
|
||||
onChange={(e) =>
|
||||
setNewUser({ ...newUser, password: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={newUser.name}
|
||||
onChange={(e) =>
|
||||
setNewUser({ ...newUser, name: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="role">Role</Label>
|
||||
<Select
|
||||
value={newUser.role}
|
||||
onValueChange={(value: "admin" | "user") =>
|
||||
setNewUser({ ...newUser, role: value as "user" })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="admin">Admin</SelectItem>
|
||||
<SelectItem value="user">User</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading === "create"}
|
||||
>
|
||||
{isLoading === "create" ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
"Create User"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<Dialog open={isBanDialogOpen} onOpenChange={setIsBanDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Ban User</DialogTitle>
|
||||
</DialogHeader>
|
||||
<form onSubmit={handleBanUser} className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="reason">Reason</Label>
|
||||
<Input
|
||||
id="reason"
|
||||
value={banForm.reason}
|
||||
onChange={(e) =>
|
||||
setBanForm({ ...banForm, reason: e.target.value })
|
||||
}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-1.5">
|
||||
<Label htmlFor="expirationDate">Expiration Date</Label>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
id="expirationDate"
|
||||
variant={"outline"}
|
||||
className={cn(
|
||||
"w-full justify-start text-left font-normal",
|
||||
!banForm.expirationDate && "text-muted-foreground",
|
||||
)}
|
||||
>
|
||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||
{banForm.expirationDate ? (
|
||||
format(banForm.expirationDate, "PPP")
|
||||
) : (
|
||||
<span>Pick a date</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto p-0">
|
||||
<Calendar
|
||||
mode="single"
|
||||
selected={banForm.expirationDate}
|
||||
onSelect={(date) =>
|
||||
setBanForm({ ...banForm, expirationDate: date })
|
||||
}
|
||||
initialFocus
|
||||
/>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading === `ban-${banForm.userId}`}
|
||||
>
|
||||
{isLoading === `ban-${banForm.userId}` ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
Banning...
|
||||
</>
|
||||
) : (
|
||||
"Ban User"
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{isUsersLoading ? (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
) : (
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Banned</TableHead>
|
||||
<TableHead>Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users?.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>{user.name}</TableCell>
|
||||
<TableCell>{user.role || "user"}</TableCell>
|
||||
<TableCell>
|
||||
{user.banned ? (
|
||||
<Badge variant="destructive">Yes</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">No</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex space-x-2">
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(user.id)}
|
||||
disabled={isLoading?.startsWith("delete")}
|
||||
>
|
||||
{isLoading === `delete-${user.id}` ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Trash className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRevokeSessions(user.id)}
|
||||
disabled={isLoading?.startsWith("revoke")}
|
||||
>
|
||||
{isLoading === `revoke-${user.id}` ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => handleImpersonateUser(user.id)}
|
||||
disabled={isLoading?.startsWith("impersonate")}
|
||||
>
|
||||
{isLoading === `impersonate-${user.id}` ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<UserCircle className="h-4 w-4 mr-2" />
|
||||
Impersonate
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
setBanForm({
|
||||
userId: user.id,
|
||||
reason: "",
|
||||
expirationDate: undefined,
|
||||
});
|
||||
if (user.banned) {
|
||||
setIsLoading(`ban-${user.id}`);
|
||||
await client.admin.unbanUser(
|
||||
{
|
||||
userId: user.id,
|
||||
},
|
||||
{
|
||||
onError(context) {
|
||||
toast.error(
|
||||
context.error.message ||
|
||||
"Failed to unban user",
|
||||
);
|
||||
setIsLoading(undefined);
|
||||
},
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["users"],
|
||||
});
|
||||
toast.success("User unbanned successfully");
|
||||
},
|
||||
},
|
||||
);
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["users"],
|
||||
});
|
||||
} else {
|
||||
setIsBanDialogOpen(true);
|
||||
}
|
||||
}}
|
||||
disabled={isLoading?.startsWith("ban")}
|
||||
>
|
||||
{isLoading === `ban-${user.id}` ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : user.banned ? (
|
||||
"Unban"
|
||||
) : (
|
||||
"Ban"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -7,10 +7,10 @@ import { OrganizationCard } from "./organization-card";
|
||||
export default async function DashboardPage() {
|
||||
const [session, activeSessions] = await Promise.all([
|
||||
auth.api.getSession({
|
||||
headers: headers(),
|
||||
headers: await headers(),
|
||||
}),
|
||||
auth.api.listSessions({
|
||||
headers: headers(),
|
||||
headers: await headers(),
|
||||
}),
|
||||
]).catch((e) => {
|
||||
throw redirect("/sign-in");
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function UserCard(props: {
|
||||
activeSessions: Session["session"][];
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { data, isPending, error } = useSession(props.session);
|
||||
const { data, isPending, error } = useSession();
|
||||
const [ua, setUa] = useState<UAParser.UAParserInstance>();
|
||||
|
||||
const session = data || props.session;
|
||||
@@ -216,7 +216,7 @@ export default function UserCard(props: {
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-sm">Two Factor</p>
|
||||
<div className="flex gap-2">
|
||||
{session?.user.twoFactorEnabled && (
|
||||
{!!session?.user.twoFactorEnabled && (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" className="gap-2">
|
||||
@@ -350,8 +350,8 @@ export default function UserCard(props: {
|
||||
setIsSignOut(true);
|
||||
await signOut({
|
||||
fetchOptions: {
|
||||
body: {
|
||||
callbackURL: "/",
|
||||
onSuccess() {
|
||||
router.push("/");
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SignInButton, SignInFallback } from "@/components/sign-in-btn";
|
||||
import { headers } from "next/headers";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default async function Home() {
|
||||
|
||||
@@ -3,21 +3,18 @@ import { SVGProps } from "react";
|
||||
export const Logo = (props: SVGProps<any>) => {
|
||||
return (
|
||||
<svg
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 200 200"
|
||||
width="60"
|
||||
height="45"
|
||||
viewBox="0 0 60 45"
|
||||
fill="none"
|
||||
className="w-5 h-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...props}
|
||||
>
|
||||
<rect width="200" height="200" fill="#D9D9D9" />
|
||||
<line x1="21.5" y1="2.18557e-08" x2="21.5" y2="200" stroke="#C4C4C4" />
|
||||
<line x1="173.5" y1="2.18557e-08" x2="173.5" y2="200" stroke="#C4C4C4" />
|
||||
<line x1="200" y1="176.5" x2="-4.37114e-08" y2="176.5" stroke="#C4C4C4" />
|
||||
<line x1="200" y1="24.5" x2="-4.37114e-08" y2="24.5" stroke="#C4C4C4" />
|
||||
<path
|
||||
d="M64.4545 135V65.1818H88.8636C93.7273 65.1818 97.7386 66.0227 100.898 67.7045C104.057 69.3636 106.409 71.6023 107.955 74.4205C109.5 77.2159 110.273 80.3182 110.273 83.7273C110.273 86.7273 109.739 89.2045 108.67 91.1591C107.625 93.1136 106.239 94.6591 104.511 95.7955C102.807 96.9318 100.955 97.7727 98.9545 98.3182V99C101.091 99.1364 103.239 99.8864 105.398 101.25C107.557 102.614 109.364 104.568 110.818 107.114C112.273 109.659 113 112.773 113 116.455C113 119.955 112.205 123.102 110.614 125.898C109.023 128.693 106.511 130.909 103.08 132.545C99.6477 134.182 95.1818 135 89.6818 135H64.4545ZM72.9091 127.5H89.6818C95.2045 127.5 99.125 126.432 101.443 124.295C103.784 122.136 104.955 119.523 104.955 116.455C104.955 114.091 104.352 111.909 103.148 109.909C101.943 107.886 100.227 106.273 98 105.068C95.7727 103.841 93.1364 103.227 90.0909 103.227H72.9091V127.5ZM72.9091 95.8636H88.5909C91.1364 95.8636 93.4318 95.3636 95.4773 94.3636C97.5455 93.3636 99.1818 91.9545 100.386 90.1364C101.614 88.3182 102.227 86.1818 102.227 83.7273C102.227 80.6591 101.159 78.0568 99.0227 75.9205C96.8864 73.7614 93.5 72.6818 88.8636 72.6818H72.9091V95.8636ZM131.665 135.545C129.983 135.545 128.54 134.943 127.335 133.739C126.131 132.534 125.528 131.091 125.528 129.409C125.528 127.727 126.131 126.284 127.335 125.08C128.54 123.875 129.983 123.273 131.665 123.273C133.347 123.273 134.79 123.875 135.994 125.08C137.199 126.284 137.801 127.727 137.801 129.409C137.801 130.523 137.517 131.545 136.949 132.477C136.403 133.409 135.665 134.159 134.733 134.727C133.824 135.273 132.801 135.545 131.665 135.545Z"
|
||||
fill="#302208"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d="M0 0H15V15H30V30H15V45H0V30V15V0ZM45 30V15H30V0H45H60V15V30V45H45H30V30H45Z"
|
||||
className="fill-black dark:fill-white"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,7 @@ import { headers } from "next/headers";
|
||||
|
||||
export async function SignInButton() {
|
||||
const session = await auth.api.getSession({
|
||||
headers: headers(),
|
||||
headers: await headers(),
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -49,9 +49,9 @@ function checkOptimisticSession(headers: Headers) {
|
||||
return !!guessIsSignIn;
|
||||
}
|
||||
|
||||
export function SignInFallback() {
|
||||
export async function SignInFallback() {
|
||||
//to avoid flash of unauthenticated state
|
||||
const guessIsSignIn = checkOptimisticSession(headers());
|
||||
const guessIsSignIn = checkOptimisticSession(await headers());
|
||||
return (
|
||||
<Link
|
||||
href={guessIsSignIn ? "/dashboard" : "/sign-in"}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { signIn } from "@/lib/auth-client";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { Key, Loader2 } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
@@ -105,60 +105,98 @@ export default function SignIn() {
|
||||
>
|
||||
{loading ? <Loader2 size={16} className="animate-spin" /> : "Login"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "github",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GitHubLogoIcon />
|
||||
Continue with Github
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.98em"
|
||||
height="1em"
|
||||
viewBox="0 0 256 262"
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "github",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
||||
/>
|
||||
<path
|
||||
fill="#EB4335"
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<GitHubLogoIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "discord",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DiscordLogoIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.98em"
|
||||
height="1em"
|
||||
viewBox="0 0 256 262"
|
||||
>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
||||
/>
|
||||
<path
|
||||
fill="#EB4335"
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "microsoft",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.passkey({
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onResponse(context) {
|
||||
router.push("/dashboard");
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { PasswordInput } from "@/components/ui/password-input";
|
||||
import { GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { DiscordLogoIcon, GitHubLogoIcon } from "@radix-ui/react-icons";
|
||||
import { useState } from "react";
|
||||
import { client, signIn, signUp } from "@/lib/auth-client";
|
||||
import Image from "next/image";
|
||||
@@ -174,74 +174,88 @@ export function SignUp() {
|
||||
"Create an account"
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
const res = await client.signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
fetchOptions: {
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
});
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.98em"
|
||||
height="1em"
|
||||
viewBox="0 0 256 262"
|
||||
>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
||||
/>
|
||||
<path
|
||||
fill="#EB4335"
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
/>
|
||||
</svg>
|
||||
Continue with Google
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social(
|
||||
{
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "github",
|
||||
callbackURL: "/dashboard",
|
||||
},
|
||||
{
|
||||
onRequest: () => {
|
||||
setLoading(true);
|
||||
},
|
||||
onResponse: () => {
|
||||
setLoading(false);
|
||||
},
|
||||
},
|
||||
);
|
||||
}}
|
||||
disabled={loading}
|
||||
>
|
||||
<GitHubLogoIcon />
|
||||
Continue with Github
|
||||
</Button>
|
||||
});
|
||||
}}
|
||||
>
|
||||
<GitHubLogoIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "discord",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DiscordLogoIcon />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "google",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="0.98em"
|
||||
height="1em"
|
||||
viewBox="0 0 256 262"
|
||||
>
|
||||
<path
|
||||
fill="#4285F4"
|
||||
d="M255.878 133.451c0-10.734-.871-18.567-2.756-26.69H130.55v48.448h71.947c-1.45 12.04-9.283 30.172-26.69 42.356l-.244 1.622l38.755 30.023l2.685.268c24.659-22.774 38.875-56.282 38.875-96.027"
|
||||
/>
|
||||
<path
|
||||
fill="#34A853"
|
||||
d="M130.55 261.1c35.248 0 64.839-11.605 86.453-31.622l-41.196-31.913c-11.024 7.688-25.82 13.055-45.257 13.055c-34.523 0-63.824-22.773-74.269-54.25l-1.531.13l-40.298 31.187l-.527 1.465C35.393 231.798 79.49 261.1 130.55 261.1"
|
||||
/>
|
||||
<path
|
||||
fill="#FBBC05"
|
||||
d="M56.281 156.37c-2.756-8.123-4.351-16.827-4.351-25.82c0-8.994 1.595-17.697 4.206-25.82l-.073-1.73L15.26 71.312l-1.335.635C5.077 89.644 0 109.517 0 130.55s5.077 40.905 13.925 58.602z"
|
||||
/>
|
||||
<path
|
||||
fill="#EB4335"
|
||||
d="M130.55 50.479c24.514 0 41.05 10.589 50.479 19.438l36.844-35.974C195.245 12.91 165.798 0 130.55 0C79.49 0 35.393 29.301 13.925 71.947l42.211 32.783c10.59-31.477 39.891-54.251 74.414-54.251"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full gap-2"
|
||||
onClick={async () => {
|
||||
await signIn.social({
|
||||
provider: "microsoft",
|
||||
callbackURL: "/dashboard",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M2 3h9v9H2zm9 19H2v-9h9zM21 3v9h-9V3zm0 19h-9v-9h9z"
|
||||
></path>
|
||||
</svg>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
organizationClient,
|
||||
passkeyClient,
|
||||
twoFactorClient,
|
||||
adminClient,
|
||||
} from "better-auth/client/plugins";
|
||||
import { toast } from "sonner";
|
||||
|
||||
@@ -10,9 +11,11 @@ export const client = createAuthClient({
|
||||
plugins: [
|
||||
organizationClient(),
|
||||
twoFactorClient({
|
||||
redirect: true,
|
||||
twoFactorPage: "/two-factor",
|
||||
}),
|
||||
passkeyClient(),
|
||||
adminClient(),
|
||||
],
|
||||
fetchOptions: {
|
||||
onError(e) {
|
||||
|
||||
@@ -1,37 +1,45 @@
|
||||
import { betterAuth } from "better-auth";
|
||||
import { organization, passkey, twoFactor } from "better-auth/plugins";
|
||||
import {
|
||||
bearer,
|
||||
organization,
|
||||
passkey,
|
||||
twoFactor,
|
||||
admin,
|
||||
} from "better-auth/plugins";
|
||||
import { reactInvitationEmail } from "./email/invitation";
|
||||
import { LibsqlDialect } from "@libsql/kysely-libsql";
|
||||
import { reactResetPasswordEmail } from "./email/rest-password";
|
||||
import { resend } from "./email/resend";
|
||||
|
||||
const from = process.env.BETTER_AUTH_EMAIL || "delivered@resend.dev";
|
||||
const to = process.env.TEST_EMAIL || "";
|
||||
|
||||
const libsql = new LibsqlDialect({
|
||||
url: process.env.TURSO_DATABASE_URL || "",
|
||||
authToken: process.env.TURSO_AUTH_TOKEN || "",
|
||||
});
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: {
|
||||
provider: "sqlite",
|
||||
url: "./db.sqlite",
|
||||
dialect: libsql,
|
||||
type: "sqlite",
|
||||
},
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
async sendResetPassword(token, user) {
|
||||
const res = await resend.emails.send({
|
||||
async sendResetPassword(url, user) {
|
||||
await resend.emails.send({
|
||||
from,
|
||||
to: user.email,
|
||||
subject: "Reset your password",
|
||||
react: reactResetPasswordEmail({
|
||||
username: user.email,
|
||||
resetLink: `${
|
||||
process.env.NODE_ENV === "development"
|
||||
? "http://localhost:3000"
|
||||
: process.env.NEXT_PUBLIC_APP_URL ||
|
||||
process.env.VERCEL_URL ||
|
||||
process.env.BETTER_AUTH_URL
|
||||
}/reset-password/${token}`,
|
||||
resetLink: url,
|
||||
}),
|
||||
});
|
||||
},
|
||||
sendEmailVerificationOnSignUp: true,
|
||||
async sendVerificationEmail(email, url) {
|
||||
console.log("Sending verification email to", email);
|
||||
const res = await resend.emails.send({
|
||||
from,
|
||||
to: to || email,
|
||||
@@ -68,12 +76,19 @@ export const auth = betterAuth({
|
||||
}),
|
||||
twoFactor({
|
||||
otpOptions: {
|
||||
sendOTP(user, otp) {
|
||||
console.log({ otp });
|
||||
async sendOTP(user, otp) {
|
||||
await resend.emails.send({
|
||||
from,
|
||||
to: user.email,
|
||||
subject: "Your OTP",
|
||||
html: `Your OTP is ${otp}`,
|
||||
});
|
||||
},
|
||||
},
|
||||
}),
|
||||
passkey(),
|
||||
bearer(),
|
||||
admin(),
|
||||
],
|
||||
socialProviders: {
|
||||
github: {
|
||||
@@ -84,5 +99,13 @@ export const auth = betterAuth({
|
||||
clientId: process.env.GOOGLE_CLIENT_ID || "",
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET || "",
|
||||
},
|
||||
discord: {
|
||||
clientId: process.env.DISCORD_CLIENT_ID || "",
|
||||
clientSecret: process.env.DISCORD_CLIENT_SECRET || "",
|
||||
},
|
||||
microsoft: {
|
||||
clientId: process.env.MICROSOFT_CLIENT_ID || "",
|
||||
clientSecret: process.env.MICROSOFT_CLIENT_SECRET || "",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "@better-auth/next",
|
||||
"name": "@better-auth/demo",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "pnpm migrate && next dev",
|
||||
"migrate": "better-auth migrate",
|
||||
"dev": "next dev",
|
||||
"dev:secure": "next dev --experimental-https",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
@@ -46,13 +47,14 @@
|
||||
"@react-email/components": "^0.0.25",
|
||||
"@react-three/fiber": "^8.17.7",
|
||||
"@tanstack/react-query": "^5.56.2",
|
||||
"better-auth": "workspace:*",
|
||||
"@types/better-sqlite3": "^7.6.11",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"better-auth": "workspace:*",
|
||||
"better-call": "0.2.3-beta.2",
|
||||
"better-sqlite3": "^11.3.0",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.0.0",
|
||||
"consola": "^3.2.3",
|
||||
"date-fns": "^3.6.0",
|
||||
"embla-carousel-react": "^8.2.1",
|
||||
"framer-motion": "^11.5.4",
|
||||
@@ -61,7 +63,7 @@
|
||||
"kysely": "^0.27.4",
|
||||
"lucide-react": "^0.439.0",
|
||||
"mini-svg-data-uri": "^1.4.4",
|
||||
"next": "15.0.0-canary.157",
|
||||
"next": "15.0.0-canary.185",
|
||||
"next-themes": "^0.3.0",
|
||||
"prisma": "^5.19.1",
|
||||
"react": "19.0.0-rc-7771d3a7-20240827",
|
||||
|
||||
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 7.6 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 369 B After Width: | Height: | Size: 186 B |
|
Before Width: | Height: | Size: 664 B After Width: | Height: | Size: 243 B |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
BIN
examples/nextjs-example/public/favicon/light/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 186 B |
BIN
examples/nextjs-example/public/favicon/light/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 243 B |
BIN
examples/nextjs-example/public/favicon/light/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "",
|
||||
"short_name": "",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||