Skip to content

Commit f9652b4

Browse files
authored
feat: v1 — persistent shell, model gateway, artifact improvements (#1462)
1 parent 3651670 commit f9652b4

File tree

161 files changed

+5156
-7999
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+5156
-7999
lines changed

.env.example

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,15 @@
1-
# Generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
2-
BETTER_AUTH_SECRET=****
3-
BETTER_AUTH_URL=****
1+
# generate a random secret: https://generate-secret.vercel.app/32 or `openssl rand -base64 32`
2+
AUTH_SECRET=****
43

5-
# The following keys below are automatically created and
6-
# added to your environment when you deploy on Vercel
7-
8-
# Instructions to create an AI Gateway API key here: https://vercel.com/ai-gateway
9-
# API key required for non-Vercel deployments
10-
# For Vercel deployments, OIDC tokens are used automatically
4+
# required for non-vercel deployments, vercel uses OIDC automatically
115
# https://vercel.com/ai-gateway
126
AI_GATEWAY_API_KEY=****
137

14-
15-
# Instructions to create a Vercel Blob Store here: https://vercel.com/docs/vercel-blob
8+
# https://vercel.com/docs/vercel-blob
169
BLOB_READ_WRITE_TOKEN=****
1710

18-
# Instructions to create a PostgreSQL database here: https://vercel.com/docs/postgres
11+
# https://vercel.com/docs/postgres
1912
POSTGRES_URL=****
2013

21-
22-
# Instructions to create a Redis store here:
2314
# https://vercel.com/docs/redis
2415
REDIS_URL=****

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
- name: Install pnpm
1414
uses: pnpm/action-setup@v4
1515
with:
16-
version: 9.12.3
16+
version: 10.32.1
1717
- name: Use Node.js ${{ matrix.node-version }}
1818
uses: actions/setup-node@v4
1919
with:

.gitignore

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,27 @@
1-
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2-
3-
# dependencies
41
node_modules
52
.pnp
63
.pnp.js
7-
8-
# testing
94
coverage
10-
11-
# next.js
125
.next/
136
out/
147
build
15-
16-
# misc
178
.DS_Store
189
*.pem
19-
20-
# debug
2110
npm-debug.log*
2211
yarn-debug.log*
2312
yarn-error.log*
2413
.pnpm-debug.log*
25-
26-
27-
# local env files
2814
.env.local
2915
.env.development.local
3016
.env.test.local
3117
.env.production.local
32-
33-
# turbo
3418
.turbo
35-
3619
.env
3720
.vercel
3821
.env*.local
39-
40-
# Playwright
4122
/test-results/
4223
/playwright-report/
4324
/blob-report/
4425
/playwright/*
45-
46-
next-env.d.ts
26+
next-env.d.ts
27+
tsconfig.tsbuildinfo

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
## Model Providers
3838

39-
This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. The default model is [OpenAI](https://openai.com) GPT-4.1 Mini, with support for Anthropic, Google, and xAI models.
39+
This template uses the [Vercel AI Gateway](https://vercel.com/docs/ai-gateway) to access multiple AI models through a unified interface. Models are configured in `lib/ai/models.ts` with per-model provider routing. Included models: Mistral, Moonshot, DeepSeek, OpenAI, and xAI.
4040

4141
### AI Gateway Authentication
4242

app/(auth)/actions.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
"use server";
22

33
import { z } from "zod";
4-
import { auth } from "@/lib/auth";
4+
5+
import { createUser, getUser } from "@/lib/db/queries";
6+
7+
import { signIn } from "./auth";
58

69
const authFormSchema = z.object({
710
email: z.string().email(),
@@ -22,11 +25,10 @@ export const login = async (
2225
password: formData.get("password"),
2326
});
2427

25-
await auth.api.signInEmail({
26-
body: {
27-
email: validatedData.email,
28-
password: validatedData.password,
29-
},
28+
await signIn("credentials", {
29+
email: validatedData.email,
30+
password: validatedData.password,
31+
redirect: false,
3032
});
3133

3234
return { status: "success" };
@@ -59,29 +61,24 @@ export const register = async (
5961
password: formData.get("password"),
6062
});
6163

62-
const result = await auth.api.signUpEmail({
63-
body: {
64-
email: validatedData.email,
65-
password: validatedData.password,
66-
name: validatedData.email,
67-
},
68-
});
64+
const [user] = await getUser(validatedData.email);
6965

70-
if (!result) {
71-
return { status: "failed" };
66+
if (user) {
67+
return { status: "user_exists" } as RegisterActionState;
7268
}
69+
await createUser(validatedData.email, validatedData.password);
70+
await signIn("credentials", {
71+
email: validatedData.email,
72+
password: validatedData.password,
73+
redirect: false,
74+
});
7375

7476
return { status: "success" };
7577
} catch (error) {
7678
if (error instanceof z.ZodError) {
7779
return { status: "invalid_data" };
7880
}
7981

80-
const message = error instanceof Error ? error.message : "";
81-
if (message.includes("already exists") || message.includes("UNIQUE")) {
82-
return { status: "user_exists" };
83-
}
84-
8582
return { status: "failed" };
8683
}
8784
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { GET, POST } from "@/app/(auth)/auth";

app/(auth)/api/auth/guest/route.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { NextResponse } from "next/server";
2+
import { getToken } from "next-auth/jwt";
3+
import { signIn } from "@/app/(auth)/auth";
4+
import { isDevelopmentEnvironment } from "@/lib/constants";
5+
6+
export async function GET(request: Request) {
7+
const { searchParams } = new URL(request.url);
8+
const rawRedirect = searchParams.get("redirectUrl") || "/";
9+
const redirectUrl =
10+
rawRedirect.startsWith("/") && !rawRedirect.startsWith("//")
11+
? rawRedirect
12+
: "/";
13+
14+
const token = await getToken({
15+
req: request,
16+
secret: process.env.AUTH_SECRET,
17+
secureCookie: !isDevelopmentEnvironment,
18+
});
19+
20+
if (token) {
21+
const base = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
22+
return NextResponse.redirect(new URL(`${base}/`, request.url));
23+
}
24+
25+
return signIn("guest", { redirect: true, redirectTo: redirectUrl });
26+
}

app/(auth)/auth.config.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { NextAuthConfig } from "next-auth";
2+
3+
const base = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
4+
5+
export const authConfig = {
6+
basePath: "/api/auth",
7+
trustHost: true,
8+
pages: {
9+
signIn: `${base}/login`,
10+
newUser: `${base}/`,
11+
},
12+
providers: [],
13+
callbacks: {},
14+
} satisfies NextAuthConfig;

app/(auth)/auth.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { compare } from "bcrypt-ts";
2+
import NextAuth, { type DefaultSession } from "next-auth";
3+
import type { DefaultJWT } from "next-auth/jwt";
4+
import Credentials from "next-auth/providers/credentials";
5+
import { DUMMY_PASSWORD } from "@/lib/constants";
6+
import { createGuestUser, getUser } from "@/lib/db/queries";
7+
import { authConfig } from "./auth.config";
8+
9+
export type UserType = "guest" | "regular";
10+
11+
declare module "next-auth" {
12+
interface Session extends DefaultSession {
13+
user: {
14+
id: string;
15+
type: UserType;
16+
} & DefaultSession["user"];
17+
}
18+
19+
interface User {
20+
id?: string;
21+
email?: string | null;
22+
type: UserType;
23+
}
24+
}
25+
26+
declare module "next-auth/jwt" {
27+
interface JWT extends DefaultJWT {
28+
id: string;
29+
type: UserType;
30+
}
31+
}
32+
33+
export const {
34+
handlers: { GET, POST },
35+
auth,
36+
signIn,
37+
signOut,
38+
} = NextAuth({
39+
...authConfig,
40+
providers: [
41+
Credentials({
42+
credentials: {
43+
email: { label: "Email", type: "email" },
44+
password: { label: "Password", type: "password" },
45+
},
46+
async authorize(credentials) {
47+
const email = String(credentials.email ?? "");
48+
const password = String(credentials.password ?? "");
49+
const users = await getUser(email);
50+
51+
if (users.length === 0) {
52+
await compare(password, DUMMY_PASSWORD);
53+
return null;
54+
}
55+
56+
const [user] = users;
57+
58+
if (!user.password) {
59+
await compare(password, DUMMY_PASSWORD);
60+
return null;
61+
}
62+
63+
const passwordsMatch = await compare(password, user.password);
64+
65+
if (!passwordsMatch) {
66+
return null;
67+
}
68+
69+
return { ...user, type: "regular" };
70+
},
71+
}),
72+
Credentials({
73+
id: "guest",
74+
credentials: {},
75+
async authorize() {
76+
const [guestUser] = await createGuestUser();
77+
return { ...guestUser, type: "guest" };
78+
},
79+
}),
80+
],
81+
callbacks: {
82+
jwt({ token, user }) {
83+
if (user) {
84+
token.id = user.id as string;
85+
token.type = user.type;
86+
}
87+
88+
return token;
89+
},
90+
session({ session, token }) {
91+
if (session.user) {
92+
session.user.id = token.id;
93+
session.user.type = token.type;
94+
}
95+
96+
return session;
97+
},
98+
},
99+
});

app/(auth)/layout.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ArrowLeftIcon } from "lucide-react";
2+
import Link from "next/link";
3+
import { SparklesIcon, VercelIcon } from "@/components/chat/icons";
4+
import { Preview } from "@/components/chat/preview";
5+
6+
export default function AuthLayout({
7+
children,
8+
}: {
9+
children: React.ReactNode;
10+
}) {
11+
return (
12+
<div className="flex h-dvh w-screen bg-sidebar">
13+
<div className="flex w-full flex-col bg-background p-8 xl:w-[600px] xl:shrink-0 xl:rounded-r-2xl xl:border-r xl:border-border/40 md:p-16">
14+
<Link
15+
className="flex w-fit items-center gap-1.5 text-[13px] text-muted-foreground transition-colors hover:text-foreground"
16+
href="/"
17+
>
18+
<ArrowLeftIcon className="size-3.5" />
19+
Back
20+
</Link>
21+
<div className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center gap-10">
22+
<div className="flex flex-col gap-2">
23+
<div className="mb-2 flex size-9 items-center justify-center rounded-lg bg-muted/60 text-muted-foreground ring-1 ring-border/50">
24+
<SparklesIcon size={14} />
25+
</div>
26+
{children}
27+
</div>
28+
</div>
29+
</div>
30+
31+
<div className="hidden flex-1 flex-col overflow-hidden pl-12 xl:flex">
32+
<div className="flex items-center gap-1.5 pt-8 text-[13px] text-muted-foreground/50">
33+
Powered by
34+
<VercelIcon size={14} />
35+
<span className="font-medium text-muted-foreground">AI Gateway</span>
36+
</div>
37+
<div className="flex-1 pt-4">
38+
<Preview />
39+
</div>
40+
</div>
41+
</div>
42+
);
43+
}

0 commit comments

Comments
 (0)