h4ckath0n 0.1.0
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/bin/cli.js +189 -0
- package/lib/scaffold.js +144 -0
- package/package.json +38 -0
- package/templates/fullstack/README.md +56 -0
- package/templates/fullstack/backend/.python-version +1 -0
- package/templates/fullstack/backend/app/__init__.py +1 -0
- package/templates/fullstack/backend/app/cli.py +98 -0
- package/templates/fullstack/backend/app/main.py +7 -0
- package/templates/fullstack/backend/app/middleware.py +55 -0
- package/templates/fullstack/backend/pyproject.toml +19 -0
- package/templates/fullstack/web/eslint.config.js +22 -0
- package/templates/fullstack/web/index.html +13 -0
- package/templates/fullstack/web/package-lock.json +5133 -0
- package/templates/fullstack/web/package.json +42 -0
- package/templates/fullstack/web/public/vite.svg +4 -0
- package/templates/fullstack/web/src/App.tsx +45 -0
- package/templates/fullstack/web/src/auth/AuthContext.tsx +238 -0
- package/templates/fullstack/web/src/auth/__tests__/token.test.ts +22 -0
- package/templates/fullstack/web/src/auth/__tests__/webauthn.test.ts +44 -0
- package/templates/fullstack/web/src/auth/api.ts +63 -0
- package/templates/fullstack/web/src/auth/deviceKey.ts +71 -0
- package/templates/fullstack/web/src/auth/index.ts +26 -0
- package/templates/fullstack/web/src/auth/token.ts +59 -0
- package/templates/fullstack/web/src/auth/webauthn.ts +133 -0
- package/templates/fullstack/web/src/auth/ws.ts +40 -0
- package/templates/fullstack/web/src/components/Alert.tsx +35 -0
- package/templates/fullstack/web/src/components/Button.tsx +37 -0
- package/templates/fullstack/web/src/components/Card.tsx +22 -0
- package/templates/fullstack/web/src/components/Input.tsx +27 -0
- package/templates/fullstack/web/src/components/Layout.tsx +88 -0
- package/templates/fullstack/web/src/components/ProtectedRoute.tsx +34 -0
- package/templates/fullstack/web/src/components/index.ts +6 -0
- package/templates/fullstack/web/src/index.css +48 -0
- package/templates/fullstack/web/src/main.tsx +28 -0
- package/templates/fullstack/web/src/pages/Admin.tsx +43 -0
- package/templates/fullstack/web/src/pages/Dashboard.tsx +75 -0
- package/templates/fullstack/web/src/pages/Landing.tsx +73 -0
- package/templates/fullstack/web/src/pages/Login.tsx +66 -0
- package/templates/fullstack/web/src/pages/Register.tsx +80 -0
- package/templates/fullstack/web/src/pages/Settings.tsx +172 -0
- package/templates/fullstack/web/src/pages/index.ts +6 -0
- package/templates/fullstack/web/src/test/setup.ts +1 -0
- package/templates/fullstack/web/src/vite-env.d.ts +1 -0
- package/templates/fullstack/web/tsconfig.app.json +21 -0
- package/templates/fullstack/web/tsconfig.json +7 -0
- package/templates/fullstack/web/tsconfig.node.json +18 -0
- package/templates/fullstack/web/vite.config.ts +16 -0
- package/templates/fullstack/web/vitest.config.ts +9 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { Link } from "react-router";
|
|
2
|
+
import { Shield, Fingerprint, Zap, Lock } from "lucide-react";
|
|
3
|
+
import { useAuth } from "../auth";
|
|
4
|
+
import { Button } from "../components/Button";
|
|
5
|
+
|
|
6
|
+
export function Landing() {
|
|
7
|
+
const { isAuthenticated } = useAuth();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<div className="max-w-3xl mx-auto text-center py-16">
|
|
11
|
+
<div className="flex justify-center mb-6">
|
|
12
|
+
<div className="p-4 bg-primary/10 rounded-3xl">
|
|
13
|
+
<Shield className="w-12 h-12 text-primary" />
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
|
|
17
|
+
<h1 className="text-4xl sm:text-5xl font-bold text-text mb-4">
|
|
18
|
+
Welcome to <span className="text-primary">{"{{PROJECT_NAME}}"}</span>
|
|
19
|
+
</h1>
|
|
20
|
+
|
|
21
|
+
<p className="text-lg text-text-muted mb-8 max-w-xl mx-auto">
|
|
22
|
+
A secure-by-default hackathon starter with passkey authentication,
|
|
23
|
+
device-bound keys, and role-based access control.
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
<div className="flex justify-center gap-4 mb-16">
|
|
27
|
+
{isAuthenticated ? (
|
|
28
|
+
<Link to="/dashboard">
|
|
29
|
+
<Button size="lg">Go to Dashboard</Button>
|
|
30
|
+
</Link>
|
|
31
|
+
) : (
|
|
32
|
+
<>
|
|
33
|
+
<Link to="/register">
|
|
34
|
+
<Button size="lg">
|
|
35
|
+
<Fingerprint className="w-5 h-5" />
|
|
36
|
+
Get Started
|
|
37
|
+
</Button>
|
|
38
|
+
</Link>
|
|
39
|
+
<Link to="/login">
|
|
40
|
+
<Button variant="secondary" size="lg">
|
|
41
|
+
Login
|
|
42
|
+
</Button>
|
|
43
|
+
</Link>
|
|
44
|
+
</>
|
|
45
|
+
)}
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="grid sm:grid-cols-3 gap-6 text-left">
|
|
49
|
+
<div className="p-6 bg-surface-alt rounded-2xl border border-border">
|
|
50
|
+
<Fingerprint className="w-8 h-8 text-primary mb-3" />
|
|
51
|
+
<h3 className="font-semibold text-text mb-1">Passkey Auth</h3>
|
|
52
|
+
<p className="text-sm text-text-muted">
|
|
53
|
+
No passwords. Register and login with device biometrics or security keys.
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
<div className="p-6 bg-surface-alt rounded-2xl border border-border">
|
|
57
|
+
<Lock className="w-8 h-8 text-primary mb-3" />
|
|
58
|
+
<h3 className="font-semibold text-text mb-1">Device-Bound Keys</h3>
|
|
59
|
+
<p className="text-sm text-text-muted">
|
|
60
|
+
Each device has a non-extractable P-256 keypair. Tokens are signed locally and verified server-side.
|
|
61
|
+
</p>
|
|
62
|
+
</div>
|
|
63
|
+
<div className="p-6 bg-surface-alt rounded-2xl border border-border">
|
|
64
|
+
<Zap className="w-8 h-8 text-primary mb-3" />
|
|
65
|
+
<h3 className="font-semibold text-text mb-1">Ship Fast</h3>
|
|
66
|
+
<p className="text-sm text-text-muted">
|
|
67
|
+
Built on FastAPI + React + Vite. Secure defaults so you can focus on building.
|
|
68
|
+
</p>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { Fingerprint, LogIn } from "lucide-react";
|
|
4
|
+
import { useAuth } from "../auth";
|
|
5
|
+
import { Card, CardContent, CardHeader } from "../components/Card";
|
|
6
|
+
import { Button } from "../components/Button";
|
|
7
|
+
import { Alert } from "../components/Alert";
|
|
8
|
+
|
|
9
|
+
export function Login() {
|
|
10
|
+
const { login, isAuthenticated } = useAuth();
|
|
11
|
+
const [loading, setLoading] = useState(false);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
|
|
14
|
+
if (isAuthenticated) {
|
|
15
|
+
return (
|
|
16
|
+
<div className="max-w-md mx-auto py-16 text-center">
|
|
17
|
+
<Alert variant="info">You are already logged in.</Alert>
|
|
18
|
+
</div>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const handleLogin = async () => {
|
|
23
|
+
setLoading(true);
|
|
24
|
+
setError(null);
|
|
25
|
+
try {
|
|
26
|
+
await login();
|
|
27
|
+
} catch (err) {
|
|
28
|
+
setError(err instanceof Error ? err.message : "Login failed");
|
|
29
|
+
} finally {
|
|
30
|
+
setLoading(false);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="max-w-md mx-auto py-16">
|
|
36
|
+
<Card>
|
|
37
|
+
<CardHeader>
|
|
38
|
+
<div className="flex items-center gap-3">
|
|
39
|
+
<div className="p-2 bg-primary/10 rounded-xl">
|
|
40
|
+
<LogIn className="w-5 h-5 text-primary" />
|
|
41
|
+
</div>
|
|
42
|
+
<div>
|
|
43
|
+
<h2 className="text-xl font-bold text-text">Welcome Back</h2>
|
|
44
|
+
<p className="text-sm text-text-muted">Login with your passkey</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
</CardHeader>
|
|
48
|
+
<CardContent className="space-y-4">
|
|
49
|
+
{error && <Alert variant="error">{error}</Alert>}
|
|
50
|
+
|
|
51
|
+
<Button onClick={handleLogin} disabled={loading} className="w-full">
|
|
52
|
+
<Fingerprint className="w-4 h-4" />
|
|
53
|
+
{loading ? "Authenticating..." : "Login with Passkey"}
|
|
54
|
+
</Button>
|
|
55
|
+
|
|
56
|
+
<p className="text-center text-sm text-text-muted">
|
|
57
|
+
No account?{" "}
|
|
58
|
+
<Link to="/register" className="text-primary hover:underline">
|
|
59
|
+
Register
|
|
60
|
+
</Link>
|
|
61
|
+
</p>
|
|
62
|
+
</CardContent>
|
|
63
|
+
</Card>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import { Fingerprint, UserPlus } from "lucide-react";
|
|
4
|
+
import { useAuth } from "../auth";
|
|
5
|
+
import { Card, CardContent, CardHeader } from "../components/Card";
|
|
6
|
+
import { Button } from "../components/Button";
|
|
7
|
+
import { Input } from "../components/Input";
|
|
8
|
+
import { Alert } from "../components/Alert";
|
|
9
|
+
|
|
10
|
+
export function Register() {
|
|
11
|
+
const { register, isAuthenticated } = useAuth();
|
|
12
|
+
const [displayName, setDisplayName] = useState("");
|
|
13
|
+
const [loading, setLoading] = useState(false);
|
|
14
|
+
const [error, setError] = useState<string | null>(null);
|
|
15
|
+
|
|
16
|
+
if (isAuthenticated) {
|
|
17
|
+
return (
|
|
18
|
+
<div className="max-w-md mx-auto py-16 text-center">
|
|
19
|
+
<Alert variant="info">You are already registered and logged in.</Alert>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const handleRegister = async () => {
|
|
25
|
+
if (!displayName.trim()) {
|
|
26
|
+
setError("Please enter a display name");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
setLoading(true);
|
|
30
|
+
setError(null);
|
|
31
|
+
try {
|
|
32
|
+
await register(displayName.trim());
|
|
33
|
+
} catch (err) {
|
|
34
|
+
setError(err instanceof Error ? err.message : "Registration failed");
|
|
35
|
+
} finally {
|
|
36
|
+
setLoading(false);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="max-w-md mx-auto py-16">
|
|
42
|
+
<Card>
|
|
43
|
+
<CardHeader>
|
|
44
|
+
<div className="flex items-center gap-3">
|
|
45
|
+
<div className="p-2 bg-primary/10 rounded-xl">
|
|
46
|
+
<UserPlus className="w-5 h-5 text-primary" />
|
|
47
|
+
</div>
|
|
48
|
+
<div>
|
|
49
|
+
<h2 className="text-xl font-bold text-text">Create Account</h2>
|
|
50
|
+
<p className="text-sm text-text-muted">Register with a passkey</p>
|
|
51
|
+
</div>
|
|
52
|
+
</div>
|
|
53
|
+
</CardHeader>
|
|
54
|
+
<CardContent className="space-y-4">
|
|
55
|
+
{error && <Alert variant="error">{error}</Alert>}
|
|
56
|
+
|
|
57
|
+
<Input
|
|
58
|
+
label="Display Name"
|
|
59
|
+
placeholder="Enter your name"
|
|
60
|
+
value={displayName}
|
|
61
|
+
onChange={(e) => setDisplayName(e.target.value)}
|
|
62
|
+
onKeyDown={(e) => e.key === "Enter" && handleRegister()}
|
|
63
|
+
/>
|
|
64
|
+
|
|
65
|
+
<Button onClick={handleRegister} disabled={loading} className="w-full">
|
|
66
|
+
<Fingerprint className="w-4 h-4" />
|
|
67
|
+
{loading ? "Creating account..." : "Register with Passkey"}
|
|
68
|
+
</Button>
|
|
69
|
+
|
|
70
|
+
<p className="text-center text-sm text-text-muted">
|
|
71
|
+
Already have an account?{" "}
|
|
72
|
+
<Link to="/login" className="text-primary hover:underline">
|
|
73
|
+
Login
|
|
74
|
+
</Link>
|
|
75
|
+
</p>
|
|
76
|
+
</CardContent>
|
|
77
|
+
</Card>
|
|
78
|
+
</div>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { Fingerprint, Plus, Trash2, AlertCircle } from "lucide-react";
|
|
4
|
+
import { apiFetch } from "../auth";
|
|
5
|
+
import { toCreateOptions, serializeCreateResponse } from "../auth/webauthn";
|
|
6
|
+
import { Card, CardContent, CardHeader } from "../components/Card";
|
|
7
|
+
import { Button } from "../components/Button";
|
|
8
|
+
import { Alert } from "../components/Alert";
|
|
9
|
+
|
|
10
|
+
interface Passkey {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string | null;
|
|
13
|
+
created_at: string;
|
|
14
|
+
last_used_at: string | null;
|
|
15
|
+
revoked_at: string | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function Settings() {
|
|
19
|
+
const queryClient = useQueryClient();
|
|
20
|
+
const [addLoading, setAddLoading] = useState(false);
|
|
21
|
+
const [error, setError] = useState<string | null>(null);
|
|
22
|
+
const [lastPasskeyError, setLastPasskeyError] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const { data: passkeys, isLoading } = useQuery<Passkey[]>({
|
|
25
|
+
queryKey: ["passkeys"],
|
|
26
|
+
queryFn: async () => {
|
|
27
|
+
const res = await apiFetch<{ passkeys: Passkey[] }>("/auth/passkeys");
|
|
28
|
+
if (!res.ok) throw new Error("Failed to load passkeys");
|
|
29
|
+
return res.data.passkeys;
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const revokeMutation = useMutation({
|
|
34
|
+
mutationFn: async (passkeyId: string) => {
|
|
35
|
+
setLastPasskeyError(null);
|
|
36
|
+
const res = await apiFetch(`/auth/passkeys/${passkeyId}/revoke`, {
|
|
37
|
+
method: "POST",
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
const data = res.data as { error?: string; detail?: string };
|
|
41
|
+
if (data.error === "LAST_PASSKEY" || data.detail?.includes("LAST_PASSKEY")) {
|
|
42
|
+
throw new Error("LAST_PASSKEY");
|
|
43
|
+
}
|
|
44
|
+
throw new Error(data.detail || "Revoke failed");
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
onSuccess: () => {
|
|
48
|
+
queryClient.invalidateQueries({ queryKey: ["passkeys"] });
|
|
49
|
+
},
|
|
50
|
+
onError: (err: Error) => {
|
|
51
|
+
if (err.message === "LAST_PASSKEY") {
|
|
52
|
+
setLastPasskeyError(
|
|
53
|
+
"Cannot revoke your last active passkey. Add another passkey first to maintain account access."
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
setError(err.message);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const handleAddPasskey = async () => {
|
|
62
|
+
setAddLoading(true);
|
|
63
|
+
setError(null);
|
|
64
|
+
try {
|
|
65
|
+
const startRes = await apiFetch<{ options: Record<string, unknown>; flow_id: string }>(
|
|
66
|
+
"/auth/passkey/add/start",
|
|
67
|
+
{ method: "POST" }
|
|
68
|
+
);
|
|
69
|
+
if (!startRes.ok) throw new Error("Failed to start passkey addition");
|
|
70
|
+
|
|
71
|
+
const createOptions = toCreateOptions(
|
|
72
|
+
startRes.data.options as unknown as Parameters<typeof toCreateOptions>[0]
|
|
73
|
+
);
|
|
74
|
+
const credential = (await navigator.credentials.create(
|
|
75
|
+
createOptions
|
|
76
|
+
)) as PublicKeyCredential | null;
|
|
77
|
+
if (!credential) throw new Error("Passkey creation cancelled");
|
|
78
|
+
|
|
79
|
+
const finishRes = await apiFetch("/auth/passkey/add/finish", {
|
|
80
|
+
method: "POST",
|
|
81
|
+
body: JSON.stringify({
|
|
82
|
+
flow_id: startRes.data.flow_id,
|
|
83
|
+
credential: serializeCreateResponse(credential),
|
|
84
|
+
}),
|
|
85
|
+
});
|
|
86
|
+
if (!finishRes.ok) throw new Error("Failed to add passkey");
|
|
87
|
+
|
|
88
|
+
queryClient.invalidateQueries({ queryKey: ["passkeys"] });
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err instanceof Error ? err.message : "Failed to add passkey");
|
|
91
|
+
} finally {
|
|
92
|
+
setAddLoading(false);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const activePasskeys = passkeys?.filter((p) => !p.revoked_at) ?? [];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="space-y-6">
|
|
100
|
+
<div>
|
|
101
|
+
<h1 className="text-2xl font-bold text-text">Settings</h1>
|
|
102
|
+
<p className="text-text-muted">Manage your passkeys and account security</p>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{error && <Alert variant="error">{error}</Alert>}
|
|
106
|
+
{lastPasskeyError && (
|
|
107
|
+
<Alert variant="warning">
|
|
108
|
+
<div className="flex items-start gap-2">
|
|
109
|
+
<AlertCircle className="w-4 h-4 shrink-0 mt-0.5" />
|
|
110
|
+
<span>{lastPasskeyError}</span>
|
|
111
|
+
</div>
|
|
112
|
+
</Alert>
|
|
113
|
+
)}
|
|
114
|
+
|
|
115
|
+
<Card>
|
|
116
|
+
<CardHeader className="flex items-center justify-between">
|
|
117
|
+
<div className="flex items-center gap-3">
|
|
118
|
+
<Fingerprint className="w-5 h-5 text-primary" />
|
|
119
|
+
<h2 className="text-lg font-semibold text-text">Passkeys</h2>
|
|
120
|
+
<span className="text-sm text-text-muted">({activePasskeys.length} active)</span>
|
|
121
|
+
</div>
|
|
122
|
+
<Button size="sm" onClick={handleAddPasskey} disabled={addLoading}>
|
|
123
|
+
<Plus className="w-4 h-4" />
|
|
124
|
+
{addLoading ? "Adding..." : "Add Passkey"}
|
|
125
|
+
</Button>
|
|
126
|
+
</CardHeader>
|
|
127
|
+
<CardContent>
|
|
128
|
+
{isLoading ? (
|
|
129
|
+
<div className="flex justify-center py-8">
|
|
130
|
+
<div className="animate-spin rounded-full h-6 w-6 border-2 border-primary border-t-transparent" />
|
|
131
|
+
</div>
|
|
132
|
+
) : passkeys && passkeys.length > 0 ? (
|
|
133
|
+
<div className="divide-y divide-border">
|
|
134
|
+
{passkeys.map((passkey) => (
|
|
135
|
+
<div key={passkey.id} className="flex items-center justify-between py-3">
|
|
136
|
+
<div>
|
|
137
|
+
<p className="text-sm font-medium text-text">
|
|
138
|
+
{passkey.label || "Unnamed passkey"}
|
|
139
|
+
{passkey.revoked_at && (
|
|
140
|
+
<span className="ml-2 text-xs text-danger">(revoked)</span>
|
|
141
|
+
)}
|
|
142
|
+
</p>
|
|
143
|
+
<p className="text-xs text-text-muted font-mono">{passkey.id}</p>
|
|
144
|
+
<p className="text-xs text-text-muted">
|
|
145
|
+
Created: {new Date(passkey.created_at).toLocaleDateString()}
|
|
146
|
+
{passkey.last_used_at && (
|
|
147
|
+
<> | Last used: {new Date(passkey.last_used_at).toLocaleDateString()}</>
|
|
148
|
+
)}
|
|
149
|
+
</p>
|
|
150
|
+
</div>
|
|
151
|
+
{!passkey.revoked_at && (
|
|
152
|
+
<Button
|
|
153
|
+
variant="danger"
|
|
154
|
+
size="sm"
|
|
155
|
+
onClick={() => revokeMutation.mutate(passkey.id)}
|
|
156
|
+
disabled={revokeMutation.isPending}
|
|
157
|
+
>
|
|
158
|
+
<Trash2 className="w-3 h-3" />
|
|
159
|
+
Revoke
|
|
160
|
+
</Button>
|
|
161
|
+
)}
|
|
162
|
+
</div>
|
|
163
|
+
))}
|
|
164
|
+
</div>
|
|
165
|
+
) : (
|
|
166
|
+
<p className="text-sm text-text-muted py-4 text-center">No passkeys found.</p>
|
|
167
|
+
)}
|
|
168
|
+
</CardContent>
|
|
169
|
+
</Card>
|
|
170
|
+
</div>
|
|
171
|
+
);
|
|
172
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "@testing-library/jest-dom";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"useDefineForClassFields": true,
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"module": "ESNext",
|
|
7
|
+
"skipLibCheck": true,
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"isolatedModules": true,
|
|
11
|
+
"moduleDetection": "force",
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"jsx": "react-jsx",
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noUnusedLocals": true,
|
|
16
|
+
"noUnusedParameters": true,
|
|
17
|
+
"noFallthroughCasesInSwitch": true,
|
|
18
|
+
"noUncheckedIndexedAccess": true
|
|
19
|
+
},
|
|
20
|
+
"include": ["src"]
|
|
21
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2023"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"allowImportingTsExtensions": true,
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"moduleDetection": "force",
|
|
11
|
+
"noEmit": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["vite.config.ts", "vitest.config.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { defineConfig } from "vite";
|
|
2
|
+
import react from "@vitejs/plugin-react";
|
|
3
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
plugins: [react(), tailwindcss()],
|
|
7
|
+
server: {
|
|
8
|
+
port: 5173,
|
|
9
|
+
proxy: {
|
|
10
|
+
"/api": {
|
|
11
|
+
target: "http://localhost:8000",
|
|
12
|
+
changeOrigin: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
});
|