ui/ health checklist
This commit is contained in:
123
docs/technical/react-markdown.md
Normal file
123
docs/technical/react-markdown.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# react-markdown
|
||||||
|
|
||||||
|
React component to render markdown.
|
||||||
|
|
||||||
|
## Contents
|
||||||
|
|
||||||
|
- [Install](#install)
|
||||||
|
- [Use](#use)
|
||||||
|
- [API](#api)
|
||||||
|
- [Examples](#examples)
|
||||||
|
- [Plugins](#plugins)
|
||||||
|
|
||||||
|
## What is this?
|
||||||
|
|
||||||
|
This package is a React component that can be given a string of markdown that it'll safely render to React elements. You can pass plugins to change how markdown is transformed and pass components that will be used instead of normal HTML elements.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install react-markdown
|
||||||
|
```
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
Basic usage:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
|
||||||
|
const markdown = "# Hi, *Pluto*!";
|
||||||
|
|
||||||
|
<Markdown>{markdown}</Markdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
With plugins:
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
const markdown = `Just a link: www.nasa.gov.`;
|
||||||
|
|
||||||
|
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
Key props:
|
||||||
|
|
||||||
|
- `children` — markdown string to render
|
||||||
|
- `remarkPlugins` — array of remark plugins
|
||||||
|
- `rehypePlugins` — array of rehype plugins
|
||||||
|
- `components` — object mapping HTML tags to React components
|
||||||
|
- `allowedElements` — array of allowed HTML tags
|
||||||
|
- `disallowedElements` — array of disallowed HTML tags
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Using GitHub Flavored Markdown
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import remarkGfm from "remark-gfm";
|
||||||
|
|
||||||
|
const markdown = `
|
||||||
|
* [x] todo
|
||||||
|
* [ ] done
|
||||||
|
|
||||||
|
| Column 1 | Column 2 |
|
||||||
|
|----------|----------|
|
||||||
|
| Cell 1 | Cell 2 |
|
||||||
|
`;
|
||||||
|
|
||||||
|
<Markdown remarkPlugins={[remarkGfm]}>{markdown}</Markdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom Components (Syntax Highlighting)
|
||||||
|
|
||||||
|
```js
|
||||||
|
import Markdown from "react-markdown";
|
||||||
|
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
|
||||||
|
import { dark } from "react-syntax-highlighter/dist/esm/styles/prism";
|
||||||
|
|
||||||
|
const markdown = `
|
||||||
|
\`\`\`js
|
||||||
|
console.log('Hello, world!');
|
||||||
|
\`\`\`
|
||||||
|
`;
|
||||||
|
|
||||||
|
<Markdown
|
||||||
|
components={{
|
||||||
|
code(props) {
|
||||||
|
const { children, className, ...rest } = props;
|
||||||
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
return match ? (
|
||||||
|
<SyntaxHighlighter
|
||||||
|
{...rest}
|
||||||
|
PreTag="div"
|
||||||
|
children={String(children).replace(/\n$/, "")}
|
||||||
|
language={match[1]}
|
||||||
|
style={dark}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<code {...rest} className={className}>
|
||||||
|
{children}
|
||||||
|
</code>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{markdown}
|
||||||
|
</Markdown>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Plugins
|
||||||
|
|
||||||
|
Common plugins:
|
||||||
|
|
||||||
|
- `remark-gfm` — GitHub Flavored Markdown (tables, task lists, strikethrough)
|
||||||
|
- `remark-math` — Math notation support
|
||||||
|
- `rehype-katex` — Render math with KaTeX
|
||||||
|
- `rehype-highlight` — Syntax highlighting
|
||||||
|
- `rehype-raw` — Allow raw HTML (use carefully for security)
|
||||||
947
package-lock.json
generated
947
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "pnpm run db:migrate && next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
"postgres": "^3.4.7",
|
"postgres": "^3.4.7",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-markdown": "^8.0.6",
|
"react-markdown": "^10.1.0",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"zod": "^4.0.17"
|
"zod": "^4.0.17"
|
||||||
},
|
},
|
||||||
|
|||||||
126
src/app/api/diagnostics/route.ts
Normal file
126
src/app/api/diagnostics/route.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
type StatusLevel = "ok" | "warn" | "error";
|
||||||
|
|
||||||
|
interface DiagnosticsResponse {
|
||||||
|
timestamp: string;
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: boolean;
|
||||||
|
BETTER_AUTH_SECRET: boolean;
|
||||||
|
GOOGLE_CLIENT_ID: boolean;
|
||||||
|
GOOGLE_CLIENT_SECRET: boolean;
|
||||||
|
OPENAI_API_KEY: boolean;
|
||||||
|
NEXT_PUBLIC_APP_URL: boolean;
|
||||||
|
};
|
||||||
|
database: {
|
||||||
|
connected: boolean;
|
||||||
|
schemaApplied: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
auth: {
|
||||||
|
configured: boolean;
|
||||||
|
routeResponding: boolean | null;
|
||||||
|
};
|
||||||
|
ai: {
|
||||||
|
configured: boolean;
|
||||||
|
};
|
||||||
|
overallStatus: StatusLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function GET(req: Request) {
|
||||||
|
const env = {
|
||||||
|
DATABASE_URL: Boolean(process.env.DATABASE_URL),
|
||||||
|
BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET),
|
||||||
|
GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID),
|
||||||
|
GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET),
|
||||||
|
OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY),
|
||||||
|
NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Database checks
|
||||||
|
let dbConnected = false;
|
||||||
|
let schemaApplied = false;
|
||||||
|
let dbError: string | undefined;
|
||||||
|
if (env.DATABASE_URL) {
|
||||||
|
try {
|
||||||
|
const [{ db }, { sql }, schema] = await Promise.all([
|
||||||
|
import("@/lib/db"),
|
||||||
|
import("drizzle-orm"),
|
||||||
|
import("@/lib/schema"),
|
||||||
|
]);
|
||||||
|
// Ping DB
|
||||||
|
await db.execute(sql`select 1`);
|
||||||
|
dbConnected = true;
|
||||||
|
try {
|
||||||
|
// Touch a known table to verify migrations
|
||||||
|
await db.select().from(schema.user).limit(1);
|
||||||
|
schemaApplied = true;
|
||||||
|
} catch {
|
||||||
|
schemaApplied = false;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
dbConnected = false;
|
||||||
|
dbError = err instanceof Error ? err.message : "Unknown database error";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dbConnected = false;
|
||||||
|
schemaApplied = false;
|
||||||
|
dbError = "DATABASE_URL is not set";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auth route check: we consider the route responding if it returns any HTTP response
|
||||||
|
// for /api/auth/session (status codes in the 2xx-4xx range are acceptable for readiness)
|
||||||
|
const origin = (() => {
|
||||||
|
try {
|
||||||
|
return new URL(req.url).origin;
|
||||||
|
} catch {
|
||||||
|
return process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000";
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
let authRouteResponding: boolean | null = null;
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${origin}/api/auth/session`, {
|
||||||
|
method: "GET",
|
||||||
|
headers: { Accept: "application/json" },
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
authRouteResponding = res.status >= 200 && res.status < 500;
|
||||||
|
} catch {
|
||||||
|
authRouteResponding = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const authConfigured =
|
||||||
|
env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET;
|
||||||
|
const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here
|
||||||
|
|
||||||
|
const overallStatus: StatusLevel = (() => {
|
||||||
|
if (!env.DATABASE_URL || !dbConnected || !schemaApplied) return "error";
|
||||||
|
if (!authConfigured) return "error";
|
||||||
|
// AI is optional; warn if not configured
|
||||||
|
if (!aiConfigured) return "warn";
|
||||||
|
return "ok";
|
||||||
|
})();
|
||||||
|
|
||||||
|
const body: DiagnosticsResponse = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
env,
|
||||||
|
database: {
|
||||||
|
connected: dbConnected,
|
||||||
|
schemaApplied,
|
||||||
|
error: dbError,
|
||||||
|
},
|
||||||
|
auth: {
|
||||||
|
configured: authConfigured,
|
||||||
|
routeResponding: authRouteResponding,
|
||||||
|
},
|
||||||
|
ai: {
|
||||||
|
configured: aiConfigured,
|
||||||
|
},
|
||||||
|
overallStatus,
|
||||||
|
};
|
||||||
|
|
||||||
|
return NextResponse.json(body, {
|
||||||
|
status: 200,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@ import { useSession } from "@/lib/auth-client";
|
|||||||
import { useState, type ReactNode } from "react";
|
import { useState, type ReactNode } from "react";
|
||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import type { CodeComponent } from "react-markdown/lib/ast-to-react";
|
|
||||||
|
|
||||||
const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
const H1: React.FC<React.HTMLAttributes<HTMLHeadingElement>> = (props) => (
|
||||||
<h1 className="mt-2 mb-3 text-2xl font-bold" {...props} />
|
<h1 className="mt-2 mb-3 text-2xl font-bold" {...props} />
|
||||||
@@ -35,6 +34,7 @@ const Anchor: React.FC<React.AnchorHTMLAttributes<HTMLAnchorElement>> = (
|
|||||||
) => (
|
) => (
|
||||||
<a
|
<a
|
||||||
className="underline underline-offset-2 text-primary hover:opacity-90"
|
className="underline underline-offset-2 text-primary hover:opacity-90"
|
||||||
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
@@ -47,8 +47,11 @@ const Blockquote: React.FC<React.BlockquoteHTMLAttributes<HTMLElement>> = (
|
|||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
const Code: CodeComponent = ({ inline, children, ...props }) => {
|
const Code: Components["code"] = ({ children, className, ...props }) => {
|
||||||
if (inline) {
|
const match = /language-(\w+)/.exec(className || "");
|
||||||
|
const isInline = !match;
|
||||||
|
|
||||||
|
if (isInline) {
|
||||||
return (
|
return (
|
||||||
<code className="rounded bg-muted px-1 py-0.5 text-xs" {...props}>
|
<code className="rounded bg-muted px-1 py-0.5 text-xs" {...props}>
|
||||||
{children}
|
{children}
|
||||||
@@ -116,11 +119,7 @@ function renderMessageContent(message: MaybePartsMessage): ReactNode {
|
|||||||
: [];
|
: [];
|
||||||
return parts.map((p, idx) =>
|
return parts.map((p, idx) =>
|
||||||
p?.type === "text" && p.text ? (
|
p?.type === "text" && p.text ? (
|
||||||
<ReactMarkdown
|
<ReactMarkdown key={idx} components={markdownComponents}>
|
||||||
key={idx}
|
|
||||||
linkTarget="_blank"
|
|
||||||
components={markdownComponents}
|
|
||||||
>
|
|
||||||
{p.text}
|
{p.text}
|
||||||
</ReactMarkdown>
|
</ReactMarkdown>
|
||||||
) : null
|
) : null
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { SetupChecklist } from "@/components/setup-checklist";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +45,8 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6 mt-12">
|
<div className="space-y-6 mt-12">
|
||||||
|
<SetupChecklist />
|
||||||
|
|
||||||
<h3 className="text-2xl font-semibold">Next Steps</h3>
|
<h3 className="text-2xl font-semibold">Next Steps</h3>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-left">
|
||||||
<div className="p-4 border rounded-lg">
|
<div className="p-4 border rounded-lg">
|
||||||
|
|||||||
147
src/components/setup-checklist.tsx
Normal file
147
src/components/setup-checklist.tsx
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
|
type DiagnosticsResponse = {
|
||||||
|
timestamp: string;
|
||||||
|
env: {
|
||||||
|
DATABASE_URL: boolean;
|
||||||
|
BETTER_AUTH_SECRET: boolean;
|
||||||
|
GOOGLE_CLIENT_ID: boolean;
|
||||||
|
GOOGLE_CLIENT_SECRET: boolean;
|
||||||
|
OPENAI_API_KEY: boolean;
|
||||||
|
NEXT_PUBLIC_APP_URL: boolean;
|
||||||
|
};
|
||||||
|
database: {
|
||||||
|
connected: boolean;
|
||||||
|
schemaApplied: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
auth: {
|
||||||
|
configured: boolean;
|
||||||
|
routeResponding: boolean | null;
|
||||||
|
};
|
||||||
|
ai: {
|
||||||
|
configured: boolean;
|
||||||
|
};
|
||||||
|
overallStatus: "ok" | "warn" | "error";
|
||||||
|
};
|
||||||
|
|
||||||
|
function StatusIcon({ ok }: { ok: boolean }) {
|
||||||
|
return ok ? (
|
||||||
|
<span aria-label="ok" title="ok">
|
||||||
|
✅
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span aria-label="not-ok" title="not ok">
|
||||||
|
❌
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SetupChecklist() {
|
||||||
|
const [data, setData] = useState<DiagnosticsResponse | null>(null);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
try {
|
||||||
|
const res = await fetch("/api/diagnostics", { cache: "no-store" });
|
||||||
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||||
|
const json = (await res.json()) as DiagnosticsResponse;
|
||||||
|
setData(json);
|
||||||
|
} catch (e) {
|
||||||
|
setError(e instanceof Error ? e.message : "Failed to load diagnostics");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
{
|
||||||
|
key: "env",
|
||||||
|
label: "Environment variables",
|
||||||
|
ok:
|
||||||
|
!!data?.env.DATABASE_URL &&
|
||||||
|
!!data?.env.BETTER_AUTH_SECRET &&
|
||||||
|
!!data?.env.GOOGLE_CLIENT_ID &&
|
||||||
|
!!data?.env.GOOGLE_CLIENT_SECRET,
|
||||||
|
detail:
|
||||||
|
"Requires DATABASE_URL, BETTER_AUTH_SECRET, GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "db",
|
||||||
|
label: "Database connected & schema",
|
||||||
|
ok: !!data?.database.connected && !!data?.database.schemaApplied,
|
||||||
|
detail: data?.database.error
|
||||||
|
? `Error: ${data.database.error}`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "auth",
|
||||||
|
label: "Auth configured",
|
||||||
|
ok: !!data?.auth.configured,
|
||||||
|
detail:
|
||||||
|
data?.auth.routeResponding === false
|
||||||
|
? "Auth route not responding"
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "ai",
|
||||||
|
label: "AI integration (optional)",
|
||||||
|
ok: !!data?.ai.configured,
|
||||||
|
detail: !data?.ai.configured
|
||||||
|
? "Set OPENAI_API_KEY for AI chat"
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const completed = steps.filter((s) => s.ok).length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-6 border rounded-lg text-left">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Setup checklist</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{completed}/{steps.length} completed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button size="sm" onClick={load} disabled={loading} className="glow">
|
||||||
|
{loading ? "Checking..." : "Re-check"}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{error ? <div className="text-sm text-red-500">{error}</div> : null}
|
||||||
|
|
||||||
|
<ul className="space-y-2">
|
||||||
|
{steps.map((s) => (
|
||||||
|
<li key={s.key} className="flex items-start gap-2">
|
||||||
|
<div className="mt-0.5">
|
||||||
|
<StatusIcon ok={Boolean(s.ok)} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="font-medium">{s.label}</div>
|
||||||
|
{s.detail ? (
|
||||||
|
<div className="text-sm text-muted-foreground">{s.detail}</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
{data ? (
|
||||||
|
<div className="mt-4 text-xs text-muted-foreground">
|
||||||
|
Last checked: {new Date(data.timestamp).toLocaleString()}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,31 +1,35 @@
|
|||||||
import { pgTable, text, timestamp, boolean, primaryKey } from "drizzle-orm/pg-core"
|
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const user = pgTable("user", {
|
export const user = pgTable("user", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
name: text("name"),
|
name: text("name").notNull(),
|
||||||
email: text("email").unique(),
|
email: text("email").notNull().unique(),
|
||||||
emailVerified: boolean("emailVerified"),
|
emailVerified: boolean("emailVerified"),
|
||||||
image: text("image"),
|
image: text("image"),
|
||||||
createdAt: timestamp("createdAt").defaultNow(),
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const session = pgTable("session", {
|
export const session = pgTable("session", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
expiresAt: timestamp("expiresAt"),
|
expiresAt: timestamp("expiresAt").notNull(),
|
||||||
token: text("token").unique(),
|
token: text("token").notNull().unique(),
|
||||||
createdAt: timestamp("createdAt").defaultNow(),
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
ipAddress: text("ipAddress"),
|
ipAddress: text("ipAddress"),
|
||||||
userAgent: text("userAgent"),
|
userAgent: text("userAgent"),
|
||||||
userId: text("userId").references(() => user.id, { onDelete: "cascade" }),
|
userId: text("userId")
|
||||||
})
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
|
});
|
||||||
|
|
||||||
export const account = pgTable("account", {
|
export const account = pgTable("account", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
accountId: text("accountId"),
|
accountId: text("accountId").notNull(),
|
||||||
providerId: text("providerId"),
|
providerId: text("providerId").notNull(),
|
||||||
userId: text("userId").references(() => user.id, { onDelete: "cascade" }),
|
userId: text("userId")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id, { onDelete: "cascade" }),
|
||||||
accessToken: text("accessToken"),
|
accessToken: text("accessToken"),
|
||||||
refreshToken: text("refreshToken"),
|
refreshToken: text("refreshToken"),
|
||||||
idToken: text("idToken"),
|
idToken: text("idToken"),
|
||||||
@@ -33,15 +37,15 @@ export const account = pgTable("account", {
|
|||||||
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"),
|
||||||
scope: text("scope"),
|
scope: text("scope"),
|
||||||
password: text("password"),
|
password: text("password"),
|
||||||
createdAt: timestamp("createdAt").defaultNow(),
|
createdAt: timestamp("createdAt").notNull().defaultNow(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
updatedAt: timestamp("updatedAt").notNull().defaultNow(),
|
||||||
})
|
});
|
||||||
|
|
||||||
export const verification = pgTable("verification", {
|
export const verification = pgTable("verification", {
|
||||||
id: text("id").primaryKey(),
|
id: text("id").primaryKey(),
|
||||||
identifier: text("identifier"),
|
identifier: text("identifier").notNull(),
|
||||||
value: text("value"),
|
value: text("value").notNull(),
|
||||||
expiresAt: timestamp("expiresAt"),
|
expiresAt: timestamp("expiresAt").notNull(),
|
||||||
createdAt: timestamp("createdAt").defaultNow(),
|
createdAt: timestamp("createdAt").defaultNow(),
|
||||||
updatedAt: timestamp("updatedAt").defaultNow(),
|
updatedAt: timestamp("updatedAt").defaultNow(),
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"types": ["react", "react-dom", "node"],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./src/*"]
|
"@/*": ["./src/*"]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user