chore: add .env.exmaple on examples

This commit is contained in:
Bereket Engida
2024-10-14 20:06:03 +03:00
parent 256c8ca5ed
commit 2bc21748e7
75 changed files with 796 additions and 1225 deletions

14
demo/nextjs/.env.exmaple Normal file
View 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=

View File

@@ -30,7 +30,7 @@ yarn-debug.log*
yarn-error.log*
# env files (can opt-in for commiting if needed)
.env*
.env
# vercel
.vercel

View File

@@ -1,2 +0,0 @@
# deps
node_modules/

View File

@@ -1,11 +0,0 @@
To install dependencies:
```sh
bun install
```
To run:
```sh
bun run dev
```
open http://localhost:3000

View File

@@ -1,23 +0,0 @@
export type Auth = {
baseURL: "http://localhost:3000";
basePath: "/auth";
database: {
provider: "sqlite";
url: "./db.sqlite";
};
socialProviders: [
{
id: "github";
},
];
plugins: [
{
id: "two-factor";
endpoints: {};
},
{
id: "organization";
endpoints: {};
},
];
};

Binary file not shown.

View File

@@ -1,19 +0,0 @@
{
"name": "@dev/hono",
"scripts": {
"dev": "tsx -r dotenv/config src/index.ts",
"auth": "pnpm better-auth"
},
"dependencies": {
"@hono/node-server": "^1.12.2",
"better-auth": "workspace:*",
"@types/better-sqlite3": "^7.6.11",
"better-sqlite3": "^11.3.0",
"dotenv": "^16.4.5",
"hono": "^4.5.9",
"tsx": "^4.19.0"
},
"devDependencies": {
"@types/bun": "latest"
}
}

View File

@@ -1,25 +0,0 @@
import Database from "better-sqlite3";
import { betterAuth } from "better-auth";
import { organization, twoFactor, username } from "better-auth/plugins";
export const auth = betterAuth({
baseURL: "http://localhost:3000",
basePath: "/auth",
database: new Database("./db.sqlite"),
socialProviders: {
github: {
clientId: process.env.GITHUB_CLIENT_ID || "",
clientSecret: process.env.GITHUB_CLIENT_SECRET || "",
},
},
plugins: [
twoFactor({
issuer: "BetterAuth",
}),
organization(),
username(),
],
emailAndPassword: {
enabled: true,
},
});

View File

@@ -1,24 +0,0 @@
import { Hono } from "hono";
import { auth } from "./auth";
import { serve } from "@hono/node-server";
import { cors } from "hono/cors";
const app = new Hono();
app.use(
"/api/auth/**",
cors({
origin: "http://localhost:5173",
allowHeaders: ["Content-Type", "Authorization"],
allowMethods: ["POST", "GET", "OPTIONS"],
exposeHeaders: ["Content-Length"],
maxAge: 600,
credentials: true,
}),
);
app.on(["POST", "GET"], "/api/auth/**", (c) => {
return auth.handler(c.req.raw);
});
serve(app);

View File

@@ -1,8 +0,0 @@
{
"compilerOptions": {
"strict": true,
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
"module": "Preserve"
}
}

View File

@@ -1,24 +0,0 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,50 +0,0 @@
# React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
## Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type aware lint rules:
- Configure the top-level `parserOptions` property like this:
```js
export default tseslint.config({
languageOptions: {
// other options...
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
},
})
```
- Replace `tseslint.configs.recommended` to `tseslint.configs.recommendedTypeChecked` or `tseslint.configs.strictTypeChecked`
- Optionally add `...tseslint.configs.stylisticTypeChecked`
- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and update the config:
```js
// eslint.config.js
import react from 'eslint-plugin-react'
export default tseslint.config({
// Set the react version
settings: { react: { version: '18.3' } },
plugins: {
// Add the react plugin
react,
},
rules: {
// other rules...
// Enable its recommended rules
...react.configs.recommended.rules,
...react.configs['jsx-runtime'].rules,
},
})
```

View File

@@ -1,28 +0,0 @@
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import globals from "globals";
import tseslint from "typescript-eslint";
export default tseslint.config(
{ ignores: ["dist"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
plugins: {
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": [
"warn",
{ allowConstantExport: true },
],
},
},
);

View File

@@ -1,13 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -1,30 +0,0 @@
{
"name": "@dev/react",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"better-auth": "workspace:^",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.9.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"eslint": "^9.9.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.1",
"vite": "^5.4.1"
}
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,42 +0,0 @@
#root {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: filter 300ms;
}
.logo:hover {
filter: drop-shadow(0 0 2em #646cffaa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
@keyframes logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@media (prefers-reduced-motion: no-preference) {
a:nth-of-type(2) .logo {
animation: logo-spin infinite 20s linear;
}
}
.card {
padding: 2em;
}
.read-the-docs {
color: #888;
}

View File

@@ -1,204 +0,0 @@
import { useState } from "react";
import "./App.css";
import { auth } from "./lib/auth";
function App() {
const session = auth.useSession();
return (
<>
<p>Better Auth</p>
<div>
{session ? (
<div
style={{
borderRadius: "10px",
border: "1px solid #4B453F",
padding: "10px",
gap: "10px",
}}
>
<p>{session.user.name}</p>
<p>{session.user.username}</p>
<p>{session.user.email}</p>
<div className="flex gap-2">
{session.user.twoFactorEnabled ? (
<button
onClick={async () => {
await auth.twoFactor.disable();
}}
>
Disable 2FA
</button>
) : (
<button
onClick={async () => {
await auth.twoFactor.enable();
}}
>
Enable 2FA
</button>
)}
<button
onClick={async () => {
await auth.signOut();
}}
>
Signout
</button>
</div>
</div>
) : (
<div>
<button
onClick={async () => {
await auth.signIn.social({
provider: "github",
});
}}
>
Continue with github
</button>
<SignIn />
<SignUp />
</div>
)}
</div>
</>
);
}
export default App;
function SignUp() {
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
borderRadius: "10px",
border: "1px solid #4B453F",
padding: "20px",
marginTop: "10px",
}}
>
<input
type="email"
id="email"
placeholder="Email"
style={{
width: "100%",
}}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="name"
id="name"
placeholder="Name"
style={{
width: "100%",
}}
value={name}
onChange={(e) => setName(e.target.value)}
/>
<input
type="text"
id="username"
placeholder="username"
style={{
width: "100%",
}}
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<input
type="password"
id="password"
placeholder="Password"
style={{
width: "100%",
}}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={async () => {
await auth.signUp.username({
email,
password,
name,
username,
});
}}
>
Sign Up
</button>
</div>
);
}
function SignIn() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: "10px",
borderRadius: "10px",
border: "1px solid #4B453F",
padding: "20px",
marginTop: "10px",
}}
>
<input
type="email"
id="email"
placeholder="Email"
style={{
width: "100%",
}}
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<input
type="password"
id="password"
placeholder="Password"
style={{
width: "100%",
}}
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<button
onClick={async () => {
await auth.signIn.username({
username: email,
password,
options: {
onSuccess(context) {
console.log({
context,
});
if (context.data.twoFactorRedirect) {
alert("two factor required");
}
},
},
});
}}
>
Sign In
</button>
</div>
);
}

View File

@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

Before

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -1,68 +0,0 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

View File

@@ -1,12 +0,0 @@
import { createAuthClient } from "better-auth/react";
import { twoFactorClient, usernameClient } from "better-auth/client/plugins";
export const auth = createAuthClient({
baseURL: "http://localhost:3000/api/auth",
plugins: [
twoFactorClient({
twoFactorPage: "/two-factor",
}),
usernameClient(),
],
});

View File

@@ -1,10 +0,0 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
</StrictMode>,
);

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -1,20 +0,0 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

View File

@@ -1,7 +0,0 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}

View File

@@ -1,18 +0,0 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}

View File

@@ -1,7 +0,0 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});

175
dev/express/.gitignore vendored
View File

@@ -1,175 +0,0 @@
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
# Logs
logs
_.log
npm-debug.log_
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Caches
.cache
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# Runtime data
pids
_.pid
_.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

View File

@@ -1,15 +0,0 @@
# express
To install dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.1.27. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

View File

@@ -1,6 +0,0 @@
import Database from "better-sqlite3";
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: new Database("better_auth.db"),
});

View File

@@ -1,12 +0,0 @@
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";
const app = express();
const port = 3005;
app.get("/api/auth/*", toNodeHandler(auth));
app.listen(port, () => {
console.log(`Example app listening on port ${port}`);
});

View File

@@ -1,20 +0,0 @@
{
"name": "@better-auth/dev-express",
"private": true,
"module": "index.ts",
"type": "module",
"devDependencies": {
"@types/bun": "latest",
"@types/express": "^4.17.21"
},
"peerDependencies": {
"typescript": "^5.0.0",
"@types/better-sqlite3": "^7.6.11",
"better-sqlite3": "^11.3.0"
},
"dependencies": {
"better-auth": "workspace:*",
"express": "^4.21.0",
"tsx": "^4.19.0"
}
}

View File

@@ -1,17 +0,0 @@
{
"compilerOptions": {
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
"verbatimModuleSyntax": true,
"strict": true,
"moduleResolution": "Bundler",
"outDir": "dist",
"sourceMap": true,
"lib": ["es2022"]
}
}

View File

@@ -1,12 +0,0 @@
import { betterAuth } from "better-auth";
import { prismaAdapter } from "better-auth/adapters/prisma";
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export const auth = betterAuth({
database: prismaAdapter(prisma, {
provider: "sqlite",
}),
provider: "sqlite",
});

View File

@@ -1,9 +0,0 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./drizzle";
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "pg",
}),
});

View File

@@ -1,22 +0,0 @@
import { drizzle } from "drizzle-orm/postgres-js";
import {
pgTable,
serial,
varchar,
text,
timestamp,
integer,
boolean,
} from "drizzle-orm/pg-core";
import postgres from "postgres";
const table = pgTable("test", {
id: text("id").primaryKey(),
});
const schema = {
table,
};
export const client = postgres(process.env.POSTGRES_URL || "");
export const db = drizzle(client, { schema });

View File

@@ -1,10 +0,0 @@
{
"name": "prisma-dev",
"dependencies": {
"@prisma/client": "^5.19.1",
"better-auth": "workspace:*",
"drizzle-orm": "^0.33.0",
"postgres": "^3.4.4",
"prisma": "^5.19.1"
}
}

View File

@@ -1,49 +0,0 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:.db/dev.db"
}
model Test {
id String @id @default(cuid())
}
model user {
id String @id
name String
email String
emailVerified Boolean
image String?
createdAt DateTime
updatedAt DateTime
session session[]
account account[]
@@unique([email])
}
model session {
id String @id
expiresAt DateTime
ipAddress String?
userAgent String?
userId String
User user @relation(fields: [userId], references: [id], onDelete: Cascade)
}
model account {
id String @id
accountId String
providerId String
userId String
User user @relation(fields: [userId], references: [id], onDelete: Cascade)
accessToken String?
refreshToken String?
idToken String?
expiresAt DateTime?
password String?
}

View File

@@ -1,41 +0,0 @@
import {
pgTable,
text,
integer,
timestamp,
boolean,
} from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("emailVerified").notNull(),
image: text("image"),
createdAt: timestamp("createdAt").notNull(),
updatedAt: timestamp("updatedAt").notNull(),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expiresAt").notNull(),
ipAddress: text("ipAddress"),
userAgent: text("userAgent"),
userId: text("userId")
.notNull()
.references(() => user.id),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("accountId").notNull(),
providerId: text("providerId").notNull(),
userId: text("userId")
.notNull()
.references(() => user.id),
accessToken: text("accessToken"),
refreshToken: text("refreshToken"),
idToken: text("idToken"),
expiresAt: timestamp("expiresAt"),
password: text("password"),
});

View File

@@ -0,0 +1,3 @@
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
RESEND_API_KEY=

View 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=

View File

@@ -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

View File

@@ -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.

View File

@@ -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.");

View File

@@ -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();

View File

@@ -32,7 +32,7 @@ export default function Component() {
code: totpCode,
})
.then((res) => {
if (res.data?.status) {
if (res.data?.session) {
setSuccess(true);
setError("");
} else {

View File

@@ -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"

View 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>
);
}

View File

@@ -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");

View File

@@ -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("/");
},
},
});

View File

@@ -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() {

View File

@@ -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>
);

View File

@@ -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"}

View File

@@ -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");
},
},
});
}}
>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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 || "",
},
},
});

View File

@@ -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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 369 B

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 664 B

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 186 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 243 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -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"
}

View File

@@ -0,0 +1,3 @@
GOOGLE_CLIENT_SECRET=
GOOGLE_CLIENT_ID=
RESEND_API_KEY=

View File

@@ -90,6 +90,7 @@
"@nanostores/solid": "^0.4.2",
"@nanostores/vue": "^0.10.0",
"@noble/ciphers": "^0.6.0",
"@noble/hashes": "^1.5.0",
"@simplewebauthn/browser": "^10.0.0",
"@simplewebauthn/server": "^10.0.1",
"better-call": "0.2.6",

9
pnpm-lock.yaml generated
View File

@@ -1456,6 +1456,9 @@ importers:
'@noble/ciphers':
specifier: ^0.6.0
version: 0.6.0
'@noble/hashes':
specifier: ^1.5.0
version: 1.5.0
'@simplewebauthn/browser':
specifier: ^10.0.0
version: 10.0.0
@@ -3630,6 +3633,10 @@ packages:
'@noble/ciphers@0.6.0':
resolution: {integrity: sha512-mIbq/R9QXk5/cTfESb1OKtyFnk7oc1Om/8onA1158K9/OZUQFDEVy55jVTato+xmp3XX6F6Qh0zz0Nc1AxAlRQ==}
'@noble/hashes@1.5.0':
resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
engines: {node: ^14.21.3 || >=16}
'@node-rs/argon2-android-arm-eabi@1.7.0':
resolution: {integrity: sha512-udDqkr5P9E+wYX1SZwAVPdyfYvaF4ry9Tm+R9LkfSHbzWH0uhU6zjIwNRp7m+n4gx691rk+lqqDAIP8RLKwbhg==}
engines: {node: '>= 10'}
@@ -16299,6 +16306,8 @@ snapshots:
'@noble/ciphers@0.6.0': {}
'@noble/hashes@1.5.0': {}
'@node-rs/argon2-android-arm-eabi@1.7.0':
optional: true