create-croissant 0.1.53 → 0.1.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/template/apps/desktop/package.json +1 -0
- package/template/apps/desktop/src/main/index.ts +14 -0
- package/template/apps/desktop/src/preload/index.d.ts +4 -1
- package/template/apps/desktop/src/preload/index.ts +9 -2
- package/template/apps/desktop/src/renderer/src/components/app-sidebar.tsx +1 -5
- package/template/apps/desktop/src/renderer/src/components/login-form.tsx +30 -131
- package/template/apps/desktop/src/renderer/src/routeTree.gen.ts +0 -21
- package/template/apps/desktop/src/renderer/src/routes/__root.tsx +20 -1
- package/template/apps/platform/package.json +3 -1
- package/template/apps/platform/src/components/login-form.tsx +3 -2
- package/template/apps/platform/src/lib/auth-client-electron.ts +13 -0
- package/template/apps/platform/src/lib/auth-client.ts +3 -1
- package/template/apps/platform/src/routes/__root.tsx +20 -1
- package/template/package.json +1 -3
- package/template/packages/auth/package.json +2 -1
- package/template/apps/desktop/src/renderer/src/components/signup-form.tsx +0 -205
- package/template/apps/desktop/src/renderer/src/routes/_public/signup.tsx +0 -16
package/package.json
CHANGED
|
@@ -17,6 +17,20 @@ function createWindow(): void {
|
|
|
17
17
|
},
|
|
18
18
|
});
|
|
19
19
|
|
|
20
|
+
// Handle deep links
|
|
21
|
+
if (process.defaultApp) {
|
|
22
|
+
if (process.argv.length >= 2) {
|
|
23
|
+
app.setAsDefaultProtocolClient("desktop", process.execPath, [join(__dirname, "../../")]);
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
app.setAsDefaultProtocolClient("desktop");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
app.on("open-url", (event, url) => {
|
|
30
|
+
event.preventDefault();
|
|
31
|
+
mainWindow.webContents.send("auth-callback", url);
|
|
32
|
+
});
|
|
33
|
+
|
|
20
34
|
mainWindow.on("ready-to-show", () => {
|
|
21
35
|
mainWindow.show();
|
|
22
36
|
});
|
|
@@ -3,6 +3,9 @@ import type { ElectronAPI } from "@electron-toolkit/preload";
|
|
|
3
3
|
declare global {
|
|
4
4
|
interface Window {
|
|
5
5
|
electron: ElectronAPI;
|
|
6
|
-
api:
|
|
6
|
+
api: {
|
|
7
|
+
onAuthCallback: (callback: (url: string) => void) => () => void;
|
|
8
|
+
openExternal: (url: string) => Promise<void>;
|
|
9
|
+
};
|
|
7
10
|
}
|
|
8
11
|
}
|
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import { contextBridge } from "electron";
|
|
1
|
+
import { contextBridge, ipcRenderer, shell } from "electron";
|
|
2
2
|
import { electronAPI } from "@electron-toolkit/preload";
|
|
3
3
|
|
|
4
4
|
// Custom APIs for renderer
|
|
5
|
-
const api = {
|
|
5
|
+
const api = {
|
|
6
|
+
onAuthCallback: (callback: (url: string) => void) => {
|
|
7
|
+
const listener = (_: any, url: string) => callback(url);
|
|
8
|
+
ipcRenderer.on("auth-callback", listener);
|
|
9
|
+
return () => ipcRenderer.removeListener("auth-callback", listener);
|
|
10
|
+
},
|
|
11
|
+
openExternal: (url: string) => shell.openExternal(url),
|
|
12
|
+
};
|
|
6
13
|
|
|
7
14
|
// Use `contextBridge` APIs to expose Electron APIs to
|
|
8
15
|
// renderer only if context isolation is enabled, otherwise
|
|
@@ -58,10 +58,6 @@ export const publicNavItems = [
|
|
|
58
58
|
title: "Login",
|
|
59
59
|
url: "/login",
|
|
60
60
|
},
|
|
61
|
-
{
|
|
62
|
-
title: "Sign Up",
|
|
63
|
-
url: "/signup",
|
|
64
|
-
},
|
|
65
61
|
],
|
|
66
62
|
},
|
|
67
63
|
{
|
|
@@ -157,7 +153,7 @@ export function AppSidebar({ items = authNavItems, ...props }: AppSidebarProps)
|
|
|
157
153
|
<Settings className="h-4 w-4" />
|
|
158
154
|
</SidebarMenuButton>
|
|
159
155
|
<SidebarMenuButton
|
|
160
|
-
onClick={async (e) => {
|
|
156
|
+
onClick={async (e: React.MouseEvent) => {
|
|
161
157
|
e.preventDefault();
|
|
162
158
|
e.stopPropagation();
|
|
163
159
|
await authClient.signOut();
|
|
@@ -7,150 +7,49 @@ import {
|
|
|
7
7
|
CardHeader,
|
|
8
8
|
CardTitle,
|
|
9
9
|
} from "@workspace/ui/components/card";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
FieldDescription,
|
|
13
|
-
FieldError,
|
|
14
|
-
FieldGroup,
|
|
15
|
-
FieldLabel,
|
|
16
|
-
} from "@workspace/ui/components/field";
|
|
17
|
-
import { Input } from "@workspace/ui/components/input";
|
|
18
|
-
import { useState } from "react";
|
|
19
|
-
import { Link, useNavigate } from "@tanstack/react-router";
|
|
20
|
-
import { useForm } from "@tanstack/react-form";
|
|
21
|
-
import { z } from "zod";
|
|
22
|
-
|
|
23
|
-
import { authClient } from "@renderer/lib/auth-client";
|
|
24
|
-
|
|
25
|
-
const loginSchema = z.object({
|
|
26
|
-
email: z.string().email("Invalid email address"),
|
|
27
|
-
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
28
|
-
});
|
|
10
|
+
import { useEffect, useState } from "react";
|
|
11
|
+
import { useNavigate } from "@tanstack/react-router";
|
|
29
12
|
|
|
30
13
|
export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
|
|
31
14
|
const [loading, setLoading] = useState(false);
|
|
32
|
-
const [error, setError] = useState<string | null>(null);
|
|
33
15
|
const navigate = useNavigate();
|
|
34
16
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
setLoading(true);
|
|
45
|
-
setError(null);
|
|
46
|
-
const { error: signInError } = await authClient.signIn.email({
|
|
47
|
-
email: value.email,
|
|
48
|
-
password: value.password,
|
|
49
|
-
});
|
|
50
|
-
if (signInError) {
|
|
51
|
-
setError(signInError.message || "Failed to sign in");
|
|
52
|
-
} else {
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const unsubscribe = window.api.onAuthCallback(async (url) => {
|
|
19
|
+
const token = new URL(url).searchParams.get("token");
|
|
20
|
+
if (token) {
|
|
21
|
+
setLoading(true);
|
|
22
|
+
// Better Auth typically handles this via the setSession/proxy
|
|
23
|
+
// In deep link flow, we might need to manually set the session
|
|
24
|
+
// or let the client handle the token.
|
|
25
|
+
// For now, we redirect to dashboard and let authClient.getSession() handle it
|
|
53
26
|
navigate({ to: "/dashboard" });
|
|
27
|
+
setLoading(false);
|
|
54
28
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
});
|
|
29
|
+
});
|
|
30
|
+
return () => unsubscribe();
|
|
31
|
+
}, [navigate]);
|
|
32
|
+
|
|
33
|
+
const handleBrowserLogin = () => {
|
|
34
|
+
const platformUrl = `${import.meta.env.VITE_API_URL.replace("/api/auth", "")}/login?redirect=desktop://auth-callback`;
|
|
35
|
+
window.api.openExternal(platformUrl);
|
|
36
|
+
};
|
|
58
37
|
|
|
59
38
|
return (
|
|
60
39
|
<div className={cn("flex flex-col gap-6", className)} {...props}>
|
|
61
40
|
<Card>
|
|
62
|
-
<CardHeader>
|
|
63
|
-
<CardTitle>Login to your account</CardTitle>
|
|
64
|
-
<CardDescription>
|
|
41
|
+
<CardHeader className="text-center">
|
|
42
|
+
<CardTitle className="text-xl">Login to your account</CardTitle>
|
|
43
|
+
<CardDescription>
|
|
44
|
+
Click the button below to login via your web browser.
|
|
45
|
+
</CardDescription>
|
|
65
46
|
</CardHeader>
|
|
66
47
|
<CardContent>
|
|
67
|
-
<
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}}
|
|
73
|
-
>
|
|
74
|
-
<FieldGroup>
|
|
75
|
-
{error && <div className="rounded bg-red-100 p-2 text-sm text-red-600">{error}</div>}
|
|
76
|
-
<form.Field
|
|
77
|
-
name="email"
|
|
78
|
-
children={(field) => {
|
|
79
|
-
const isInvalid =
|
|
80
|
-
field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
81
|
-
return (
|
|
82
|
-
<Field data-invalid={isInvalid}>
|
|
83
|
-
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
84
|
-
<Input
|
|
85
|
-
id={field.name}
|
|
86
|
-
name={field.name}
|
|
87
|
-
type="email"
|
|
88
|
-
placeholder="m@example.com"
|
|
89
|
-
value={field.state.value}
|
|
90
|
-
onBlur={field.handleBlur}
|
|
91
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
92
|
-
required
|
|
93
|
-
/>
|
|
94
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
95
|
-
</Field>
|
|
96
|
-
);
|
|
97
|
-
}}
|
|
98
|
-
/>
|
|
99
|
-
<form.Field
|
|
100
|
-
name="password"
|
|
101
|
-
children={(field) => {
|
|
102
|
-
const isInvalid =
|
|
103
|
-
field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
104
|
-
return (
|
|
105
|
-
<Field data-invalid={isInvalid}>
|
|
106
|
-
<div className="flex items-center">
|
|
107
|
-
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
|
108
|
-
<a
|
|
109
|
-
href="#"
|
|
110
|
-
className="ml-auto inline-block text-sm underline-offset-4 hover:underline"
|
|
111
|
-
>
|
|
112
|
-
Forgot your password?
|
|
113
|
-
</a>
|
|
114
|
-
</div>
|
|
115
|
-
<Input
|
|
116
|
-
id={field.name}
|
|
117
|
-
name={field.name}
|
|
118
|
-
type="password"
|
|
119
|
-
value={field.state.value}
|
|
120
|
-
onBlur={field.handleBlur}
|
|
121
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
122
|
-
required
|
|
123
|
-
/>
|
|
124
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
125
|
-
</Field>
|
|
126
|
-
);
|
|
127
|
-
}}
|
|
128
|
-
/>
|
|
129
|
-
<Field>
|
|
130
|
-
<form.Subscribe
|
|
131
|
-
selector={(state) => ({
|
|
132
|
-
canSubmit: state.canSubmit,
|
|
133
|
-
isSubmitting: state.isSubmitting,
|
|
134
|
-
})}
|
|
135
|
-
>
|
|
136
|
-
{(state: { canSubmit: boolean; isSubmitting: boolean }) => (
|
|
137
|
-
<Button type="submit" disabled={!state.canSubmit || state.isSubmitting || loading}>
|
|
138
|
-
{state.isSubmitting || loading ? "Logging in..." : "Login"}
|
|
139
|
-
</Button>
|
|
140
|
-
)}
|
|
141
|
-
</form.Subscribe>
|
|
142
|
-
<Button variant="outline" type="button" disabled={loading}>
|
|
143
|
-
Login with Google
|
|
144
|
-
</Button>
|
|
145
|
-
<FieldDescription className="text-center">
|
|
146
|
-
Don't have an account?{" "}
|
|
147
|
-
<Link to="/signup" className="underline">
|
|
148
|
-
Sign up
|
|
149
|
-
</Link>
|
|
150
|
-
</FieldDescription>
|
|
151
|
-
</Field>
|
|
152
|
-
</FieldGroup>
|
|
153
|
-
</form>
|
|
48
|
+
<div className="flex flex-col gap-4">
|
|
49
|
+
<Button size="lg" className="w-full" onClick={handleBrowserLogin} disabled={loading}>
|
|
50
|
+
{loading ? "Authenticating..." : "Login from the web"}
|
|
51
|
+
</Button>
|
|
52
|
+
</div>
|
|
154
53
|
</CardContent>
|
|
155
54
|
</Card>
|
|
156
55
|
</div>
|
|
@@ -12,7 +12,6 @@ import { Route as rootRouteImport } from './routes/__root'
|
|
|
12
12
|
import { Route as PublicRouteImport } from './routes/_public'
|
|
13
13
|
import { Route as AuthRouteImport } from './routes/_auth'
|
|
14
14
|
import { Route as PublicIndexRouteImport } from './routes/_public/index'
|
|
15
|
-
import { Route as PublicSignupRouteImport } from './routes/_public/signup'
|
|
16
15
|
import { Route as PublicLoginRouteImport } from './routes/_public/login'
|
|
17
16
|
import { Route as AuthDashboardRouteImport } from './routes/_auth/dashboard'
|
|
18
17
|
import { Route as AuthAccountRouteImport } from './routes/_auth/account'
|
|
@@ -32,11 +31,6 @@ const PublicIndexRoute = PublicIndexRouteImport.update({
|
|
|
32
31
|
path: '/',
|
|
33
32
|
getParentRoute: () => PublicRoute,
|
|
34
33
|
} as any)
|
|
35
|
-
const PublicSignupRoute = PublicSignupRouteImport.update({
|
|
36
|
-
id: '/signup',
|
|
37
|
-
path: '/signup',
|
|
38
|
-
getParentRoute: () => PublicRoute,
|
|
39
|
-
} as any)
|
|
40
34
|
const PublicLoginRoute = PublicLoginRouteImport.update({
|
|
41
35
|
id: '/login',
|
|
42
36
|
path: '/login',
|
|
@@ -70,7 +64,6 @@ export interface FileRoutesByFullPath {
|
|
|
70
64
|
'/account': typeof AuthAccountRoute
|
|
71
65
|
'/dashboard': typeof AuthDashboardRoute
|
|
72
66
|
'/login': typeof PublicLoginRoute
|
|
73
|
-
'/signup': typeof PublicSignupRoute
|
|
74
67
|
'/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
|
|
75
68
|
'/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
|
|
76
69
|
}
|
|
@@ -79,7 +72,6 @@ export interface FileRoutesByTo {
|
|
|
79
72
|
'/account': typeof AuthAccountRoute
|
|
80
73
|
'/dashboard': typeof AuthDashboardRoute
|
|
81
74
|
'/login': typeof PublicLoginRoute
|
|
82
|
-
'/signup': typeof PublicSignupRoute
|
|
83
75
|
'/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
|
|
84
76
|
'/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
|
|
85
77
|
}
|
|
@@ -90,7 +82,6 @@ export interface FileRoutesById {
|
|
|
90
82
|
'/_auth/account': typeof AuthAccountRoute
|
|
91
83
|
'/_auth/dashboard': typeof AuthDashboardRoute
|
|
92
84
|
'/_public/login': typeof PublicLoginRoute
|
|
93
|
-
'/_public/signup': typeof PublicSignupRoute
|
|
94
85
|
'/_public/': typeof PublicIndexRoute
|
|
95
86
|
'/_auth/examples/client-orpc-auth': typeof AuthExamplesClientOrpcAuthRoute
|
|
96
87
|
'/_public/examples/client-orpc': typeof PublicExamplesClientOrpcRoute
|
|
@@ -102,7 +93,6 @@ export interface FileRouteTypes {
|
|
|
102
93
|
| '/account'
|
|
103
94
|
| '/dashboard'
|
|
104
95
|
| '/login'
|
|
105
|
-
| '/signup'
|
|
106
96
|
| '/examples/client-orpc-auth'
|
|
107
97
|
| '/examples/client-orpc'
|
|
108
98
|
fileRoutesByTo: FileRoutesByTo
|
|
@@ -111,7 +101,6 @@ export interface FileRouteTypes {
|
|
|
111
101
|
| '/account'
|
|
112
102
|
| '/dashboard'
|
|
113
103
|
| '/login'
|
|
114
|
-
| '/signup'
|
|
115
104
|
| '/examples/client-orpc-auth'
|
|
116
105
|
| '/examples/client-orpc'
|
|
117
106
|
id:
|
|
@@ -121,7 +110,6 @@ export interface FileRouteTypes {
|
|
|
121
110
|
| '/_auth/account'
|
|
122
111
|
| '/_auth/dashboard'
|
|
123
112
|
| '/_public/login'
|
|
124
|
-
| '/_public/signup'
|
|
125
113
|
| '/_public/'
|
|
126
114
|
| '/_auth/examples/client-orpc-auth'
|
|
127
115
|
| '/_public/examples/client-orpc'
|
|
@@ -155,13 +143,6 @@ declare module '@tanstack/react-router' {
|
|
|
155
143
|
preLoaderRoute: typeof PublicIndexRouteImport
|
|
156
144
|
parentRoute: typeof PublicRoute
|
|
157
145
|
}
|
|
158
|
-
'/_public/signup': {
|
|
159
|
-
id: '/_public/signup'
|
|
160
|
-
path: '/signup'
|
|
161
|
-
fullPath: '/signup'
|
|
162
|
-
preLoaderRoute: typeof PublicSignupRouteImport
|
|
163
|
-
parentRoute: typeof PublicRoute
|
|
164
|
-
}
|
|
165
146
|
'/_public/login': {
|
|
166
147
|
id: '/_public/login'
|
|
167
148
|
path: '/login'
|
|
@@ -216,14 +197,12 @@ const AuthRouteWithChildren = AuthRoute._addFileChildren(AuthRouteChildren)
|
|
|
216
197
|
|
|
217
198
|
interface PublicRouteChildren {
|
|
218
199
|
PublicLoginRoute: typeof PublicLoginRoute
|
|
219
|
-
PublicSignupRoute: typeof PublicSignupRoute
|
|
220
200
|
PublicIndexRoute: typeof PublicIndexRoute
|
|
221
201
|
PublicExamplesClientOrpcRoute: typeof PublicExamplesClientOrpcRoute
|
|
222
202
|
}
|
|
223
203
|
|
|
224
204
|
const PublicRouteChildren: PublicRouteChildren = {
|
|
225
205
|
PublicLoginRoute: PublicLoginRoute,
|
|
226
|
-
PublicSignupRoute: PublicSignupRoute,
|
|
227
206
|
PublicIndexRoute: PublicIndexRoute,
|
|
228
207
|
PublicExamplesClientOrpcRoute: PublicExamplesClientOrpcRoute,
|
|
229
208
|
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import { Outlet, createRootRoute } from "@tanstack/react-router";
|
|
1
|
+
import { Link, Outlet, createRootRoute } from "@tanstack/react-router";
|
|
2
2
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
3
|
import { Toaster } from "@workspace/ui/components/sonner";
|
|
4
4
|
import { ThemeProvider } from "@workspace/ui/components/theme-provider";
|
|
5
5
|
import { ORPCProvider } from "@workspace/orpc/react";
|
|
6
6
|
import { orpc } from "@renderer/lib/orpc";
|
|
7
7
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
|
8
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@workspace/ui/components/empty";
|
|
9
|
+
import { Button, buttonVariants } from "@workspace/ui/components/button";
|
|
8
10
|
|
|
9
11
|
import "@workspace/ui/globals.css";
|
|
10
12
|
|
|
@@ -12,6 +14,23 @@ const queryClient = new QueryClient();
|
|
|
12
14
|
|
|
13
15
|
export const Route = createRootRoute({
|
|
14
16
|
component: RootLayout,
|
|
17
|
+
notFoundComponent: () => {
|
|
18
|
+
return (
|
|
19
|
+
<Empty className="h-screen border-none">
|
|
20
|
+
<EmptyHeader>
|
|
21
|
+
<EmptyTitle>404 - Page Not Found</EmptyTitle>
|
|
22
|
+
<EmptyDescription>
|
|
23
|
+
The page you are looking for does not exist or has been moved.
|
|
24
|
+
</EmptyDescription>
|
|
25
|
+
</EmptyHeader>
|
|
26
|
+
<EmptyContent>
|
|
27
|
+
<Link to="/" className={buttonVariants({ variant: "outline" })}>
|
|
28
|
+
Go Home
|
|
29
|
+
</Link>
|
|
30
|
+
</EmptyContent>
|
|
31
|
+
</Empty>
|
|
32
|
+
);
|
|
33
|
+
},
|
|
15
34
|
});
|
|
16
35
|
|
|
17
36
|
function RootLayout() {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"db:studio": "drizzle-kit studio"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
+
"@better-auth/electron": "1.6.11",
|
|
15
16
|
"@noble/ciphers": "^2.2.0",
|
|
16
17
|
"@tailwindcss/vite": "^4.2.4",
|
|
17
18
|
"@tanstack/react-router": "^1.168.24",
|
|
@@ -20,11 +21,12 @@
|
|
|
20
21
|
"@workspace/auth": "workspace:*",
|
|
21
22
|
"@workspace/orpc": "workspace:*",
|
|
22
23
|
"@workspace/ui": "workspace:*",
|
|
24
|
+
"better-auth": "1.6.11",
|
|
23
25
|
"lucide-react": "^1.11.0",
|
|
24
26
|
"nitro": "latest",
|
|
25
|
-
"sonner": "^2.0.7",
|
|
26
27
|
"react": "19.2.5",
|
|
27
28
|
"react-dom": "19.2.5",
|
|
29
|
+
"sonner": "^2.0.7",
|
|
28
30
|
"tailwindcss": "^4.2.4",
|
|
29
31
|
"vite-tsconfig-paths": "^6.1.1"
|
|
30
32
|
},
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
} from "@workspace/ui/components/field";
|
|
17
17
|
import { Input } from "@workspace/ui/components/input";
|
|
18
18
|
import { useState } from "react";
|
|
19
|
-
import { Link } from "@tanstack/react-router";
|
|
19
|
+
import { Link, useSearch } from "@tanstack/react-router";
|
|
20
20
|
import { useForm } from "@tanstack/react-form";
|
|
21
21
|
import { z } from "zod";
|
|
22
22
|
|
|
@@ -30,6 +30,7 @@ const loginSchema = z.object({
|
|
|
30
30
|
export function LoginForm({ className, ...props }: React.ComponentProps<"div">) {
|
|
31
31
|
const [loading, setLoading] = useState(false);
|
|
32
32
|
const [error, setError] = useState<string | null>(null);
|
|
33
|
+
const search = useSearch({ from: "/_public/login" }) as { redirect?: string };
|
|
33
34
|
|
|
34
35
|
const form = useForm({
|
|
35
36
|
defaultValues: {
|
|
@@ -45,7 +46,7 @@ export function LoginForm({ className, ...props }: React.ComponentProps<"div">)
|
|
|
45
46
|
const { error: signInError } = await authClient.signIn.email({
|
|
46
47
|
email: value.email,
|
|
47
48
|
password: value.password,
|
|
48
|
-
callbackURL: "/dashboard",
|
|
49
|
+
callbackURL: search.redirect || "/dashboard",
|
|
49
50
|
});
|
|
50
51
|
if (signInError) {
|
|
51
52
|
setError(signInError.message || "Failed to sign in");
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { createAuthClient } from "better-auth/client";
|
|
2
|
+
import { electronProxyClient } from "@better-auth/electron/proxy";
|
|
3
|
+
|
|
4
|
+
export const authClient = createAuthClient({
|
|
5
|
+
baseURL: import.meta.env.VITE_API_URL,
|
|
6
|
+
plugins: [
|
|
7
|
+
electronProxyClient({
|
|
8
|
+
protocol: {
|
|
9
|
+
scheme: "com.example.app"
|
|
10
|
+
},
|
|
11
|
+
}),
|
|
12
|
+
],
|
|
13
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { HeadContent, Scripts, createRootRoute } from "@tanstack/react-router";
|
|
1
|
+
import { HeadContent, Link, Scripts, createRootRoute } from "@tanstack/react-router";
|
|
2
2
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
3
|
import { Toaster } from "@workspace/ui/components/sonner";
|
|
4
4
|
import { ThemeProvider } from "@workspace/ui/components/theme-provider";
|
|
5
5
|
import { ORPCProvider } from "@workspace/orpc/react";
|
|
6
6
|
import { orpc } from "@/lib/orpc";
|
|
7
|
+
import { Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyTitle } from "@workspace/ui/components/empty";
|
|
8
|
+
import { Button, buttonVariants } from "@workspace/ui/components/button";
|
|
7
9
|
|
|
8
10
|
import appCss from "@workspace/ui/globals.css?url";
|
|
9
11
|
|
|
@@ -31,6 +33,23 @@ export const Route = createRootRoute({
|
|
|
31
33
|
],
|
|
32
34
|
}),
|
|
33
35
|
shellComponent: RootDocument,
|
|
36
|
+
notFoundComponent: () => {
|
|
37
|
+
return (
|
|
38
|
+
<Empty className="h-screen border-none">
|
|
39
|
+
<EmptyHeader>
|
|
40
|
+
<EmptyTitle>404 - Page Not Found</EmptyTitle>
|
|
41
|
+
<EmptyDescription>
|
|
42
|
+
The page you are looking for does not exist or has been moved.
|
|
43
|
+
</EmptyDescription>
|
|
44
|
+
</EmptyHeader>
|
|
45
|
+
<EmptyContent>
|
|
46
|
+
<Link to="/" className={buttonVariants({ variant: "outline" })}>
|
|
47
|
+
Go Home
|
|
48
|
+
</Link>
|
|
49
|
+
</EmptyContent>
|
|
50
|
+
</Empty>
|
|
51
|
+
);
|
|
52
|
+
},
|
|
34
53
|
});
|
|
35
54
|
|
|
36
55
|
function RootDocument({ children }: { children: React.ReactNode }) {
|
package/template/package.json
CHANGED
|
@@ -25,11 +25,9 @@
|
|
|
25
25
|
"@orpc/server": "latest",
|
|
26
26
|
"@orpc/tanstack-query": "latest",
|
|
27
27
|
"@tanstack/react-form": "latest",
|
|
28
|
-
"@tanstack/react-query": "latest"
|
|
29
|
-
"better-auth": "^1.6.11"
|
|
28
|
+
"@tanstack/react-query": "latest"
|
|
30
29
|
},
|
|
31
30
|
"devDependencies": {
|
|
32
|
-
"@better-auth/core": "^1.6.11",
|
|
33
31
|
"husky": "latest",
|
|
34
32
|
"oxfmt": "latest",
|
|
35
33
|
"oxlint": "latest",
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"generate": "better-auth generate --output ../db/src/lib/auth-schema.ts"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
|
-
"@workspace/db": "workspace:*"
|
|
15
|
+
"@workspace/db": "workspace:*",
|
|
16
|
+
"better-auth": "1.6.11"
|
|
16
17
|
},
|
|
17
18
|
"devDependencies": {
|
|
18
19
|
"@better-auth/cli": "^1.4.22",
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
import { Button } from "@workspace/ui/components/button";
|
|
2
|
-
import {
|
|
3
|
-
Card,
|
|
4
|
-
CardContent,
|
|
5
|
-
CardDescription,
|
|
6
|
-
CardHeader,
|
|
7
|
-
CardTitle,
|
|
8
|
-
} from "@workspace/ui/components/card";
|
|
9
|
-
import {
|
|
10
|
-
Field,
|
|
11
|
-
FieldDescription,
|
|
12
|
-
FieldError,
|
|
13
|
-
FieldGroup,
|
|
14
|
-
FieldLabel,
|
|
15
|
-
} from "@workspace/ui/components/field";
|
|
16
|
-
import { Input } from "@workspace/ui/components/input";
|
|
17
|
-
import { useState } from "react";
|
|
18
|
-
import { Link, useNavigate } from "@tanstack/react-router";
|
|
19
|
-
import { useForm } from "@tanstack/react-form";
|
|
20
|
-
import { z } from "zod";
|
|
21
|
-
import { authClient } from "@renderer/lib/auth-client";
|
|
22
|
-
|
|
23
|
-
const signupSchema = z
|
|
24
|
-
.object({
|
|
25
|
-
name: z.string().min(1, "Name is required"),
|
|
26
|
-
email: z.string().email("Invalid email address"),
|
|
27
|
-
password: z.string().min(8, "Password must be at least 8 characters"),
|
|
28
|
-
confirmPassword: z.string().min(1, "Confirm password is required"),
|
|
29
|
-
})
|
|
30
|
-
.refine((data) => data.password === data.confirmPassword, {
|
|
31
|
-
message: "Passwords do not match",
|
|
32
|
-
path: ["confirmPassword"],
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
export function SignupForm({ ...props }: React.ComponentProps<typeof Card>) {
|
|
36
|
-
const [loading, setLoading] = useState(false);
|
|
37
|
-
const [error, setError] = useState<string | null>(null);
|
|
38
|
-
const navigate = useNavigate();
|
|
39
|
-
|
|
40
|
-
const form = useForm({
|
|
41
|
-
defaultValues: {
|
|
42
|
-
name: "",
|
|
43
|
-
email: "",
|
|
44
|
-
password: "",
|
|
45
|
-
confirmPassword: "",
|
|
46
|
-
},
|
|
47
|
-
validators: {
|
|
48
|
-
onChange: signupSchema,
|
|
49
|
-
},
|
|
50
|
-
onSubmit: async ({ value }) => {
|
|
51
|
-
setLoading(true);
|
|
52
|
-
setError(null);
|
|
53
|
-
const { error: signUpError } = await authClient.signUp.email({
|
|
54
|
-
email: value.email,
|
|
55
|
-
password: value.password,
|
|
56
|
-
name: value.name,
|
|
57
|
-
});
|
|
58
|
-
if (signUpError) {
|
|
59
|
-
setError(signUpError.message || "Failed to sign up");
|
|
60
|
-
} else {
|
|
61
|
-
navigate({ to: "/dashboard" });
|
|
62
|
-
}
|
|
63
|
-
setLoading(false);
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return (
|
|
68
|
-
<Card {...props}>
|
|
69
|
-
<CardHeader>
|
|
70
|
-
<CardTitle>Create an account</CardTitle>
|
|
71
|
-
<CardDescription>Enter your information below to create your account</CardDescription>
|
|
72
|
-
</CardHeader>
|
|
73
|
-
<CardContent>
|
|
74
|
-
<form
|
|
75
|
-
onSubmit={(e) => {
|
|
76
|
-
e.preventDefault();
|
|
77
|
-
e.stopPropagation();
|
|
78
|
-
form.handleSubmit();
|
|
79
|
-
}}
|
|
80
|
-
>
|
|
81
|
-
<FieldGroup>
|
|
82
|
-
{error && <div className="rounded bg-red-100 p-2 text-sm text-red-600">{error}</div>}
|
|
83
|
-
<form.Field
|
|
84
|
-
name="name"
|
|
85
|
-
children={(field) => {
|
|
86
|
-
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
87
|
-
return (
|
|
88
|
-
<Field data-invalid={isInvalid}>
|
|
89
|
-
<FieldLabel htmlFor={field.name}>Full Name</FieldLabel>
|
|
90
|
-
<Input
|
|
91
|
-
id={field.name}
|
|
92
|
-
name={field.name}
|
|
93
|
-
type="text"
|
|
94
|
-
placeholder="John Doe"
|
|
95
|
-
value={field.state.value}
|
|
96
|
-
onBlur={field.handleBlur}
|
|
97
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
98
|
-
required
|
|
99
|
-
/>
|
|
100
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
101
|
-
</Field>
|
|
102
|
-
);
|
|
103
|
-
}}
|
|
104
|
-
/>
|
|
105
|
-
<form.Field
|
|
106
|
-
name="email"
|
|
107
|
-
children={(field) => {
|
|
108
|
-
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
109
|
-
return (
|
|
110
|
-
<Field data-invalid={isInvalid}>
|
|
111
|
-
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
|
|
112
|
-
<Input
|
|
113
|
-
id={field.name}
|
|
114
|
-
name={field.name}
|
|
115
|
-
type="email"
|
|
116
|
-
placeholder="m@example.com"
|
|
117
|
-
value={field.state.value}
|
|
118
|
-
onBlur={field.handleBlur}
|
|
119
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
120
|
-
required
|
|
121
|
-
/>
|
|
122
|
-
<FieldDescription>
|
|
123
|
-
We'll use this to contact you. We will not share your email with anyone
|
|
124
|
-
else.
|
|
125
|
-
</FieldDescription>
|
|
126
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
127
|
-
</Field>
|
|
128
|
-
);
|
|
129
|
-
}}
|
|
130
|
-
/>
|
|
131
|
-
<form.Field
|
|
132
|
-
name="password"
|
|
133
|
-
children={(field) => {
|
|
134
|
-
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
135
|
-
return (
|
|
136
|
-
<Field data-invalid={isInvalid}>
|
|
137
|
-
<FieldLabel htmlFor={field.name}>Password</FieldLabel>
|
|
138
|
-
<Input
|
|
139
|
-
id={field.name}
|
|
140
|
-
name={field.name}
|
|
141
|
-
type="password"
|
|
142
|
-
value={field.state.value}
|
|
143
|
-
onBlur={field.handleBlur}
|
|
144
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
145
|
-
required
|
|
146
|
-
/>
|
|
147
|
-
<FieldDescription>Must be at least 8 characters long.</FieldDescription>
|
|
148
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
149
|
-
</Field>
|
|
150
|
-
);
|
|
151
|
-
}}
|
|
152
|
-
/>
|
|
153
|
-
<form.Field
|
|
154
|
-
name="confirmPassword"
|
|
155
|
-
children={(field) => {
|
|
156
|
-
const isInvalid = field.state.meta.isTouched && field.state.meta.errors.length > 0;
|
|
157
|
-
return (
|
|
158
|
-
<Field data-invalid={isInvalid}>
|
|
159
|
-
<FieldLabel htmlFor={field.name}>Confirm Password</FieldLabel>
|
|
160
|
-
<Input
|
|
161
|
-
id={field.name}
|
|
162
|
-
name={field.name}
|
|
163
|
-
type="password"
|
|
164
|
-
value={field.state.value}
|
|
165
|
-
onBlur={field.handleBlur}
|
|
166
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
167
|
-
required
|
|
168
|
-
/>
|
|
169
|
-
<FieldDescription>Please confirm your password.</FieldDescription>
|
|
170
|
-
{isInvalid && <FieldError errors={field.state.meta.errors} />}
|
|
171
|
-
</Field>
|
|
172
|
-
);
|
|
173
|
-
}}
|
|
174
|
-
/>
|
|
175
|
-
<FieldGroup>
|
|
176
|
-
<Field>
|
|
177
|
-
<form.Subscribe
|
|
178
|
-
selector={(state) => ({
|
|
179
|
-
canSubmit: state.canSubmit,
|
|
180
|
-
isSubmitting: state.isSubmitting,
|
|
181
|
-
})}
|
|
182
|
-
>
|
|
183
|
-
{(state: { canSubmit: boolean; isSubmitting: boolean }) => (
|
|
184
|
-
<Button type="submit" disabled={!state.canSubmit || state.isSubmitting || loading}>
|
|
185
|
-
{state.isSubmitting || loading ? "Creating..." : "Create Account"}
|
|
186
|
-
</Button>
|
|
187
|
-
)}
|
|
188
|
-
</form.Subscribe>
|
|
189
|
-
<Button variant="outline" type="button" disabled={loading}>
|
|
190
|
-
Sign up with Google
|
|
191
|
-
</Button>
|
|
192
|
-
<FieldDescription className="px-6 text-center">
|
|
193
|
-
Already have an account?{" "}
|
|
194
|
-
<Link to="/login" className="underline">
|
|
195
|
-
Sign in
|
|
196
|
-
</Link>
|
|
197
|
-
</FieldDescription>
|
|
198
|
-
</Field>
|
|
199
|
-
</FieldGroup>
|
|
200
|
-
</FieldGroup>
|
|
201
|
-
</form>
|
|
202
|
-
</CardContent>
|
|
203
|
-
</Card>
|
|
204
|
-
);
|
|
205
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
2
|
-
import { SignupForm } from "@renderer/components/signup-form";
|
|
3
|
-
|
|
4
|
-
export const Route = createFileRoute("/_public/signup")({
|
|
5
|
-
component: Signup,
|
|
6
|
-
});
|
|
7
|
-
|
|
8
|
-
function Signup() {
|
|
9
|
-
return (
|
|
10
|
-
<div className="flex min-h-svh items-center justify-center p-6">
|
|
11
|
-
<div className="w-full max-w-sm">
|
|
12
|
-
<SignupForm />
|
|
13
|
-
</div>
|
|
14
|
-
</div>
|
|
15
|
-
);
|
|
16
|
-
}
|