better-ts-stack 2.0.0 → 2.0.1
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/LICENSE +21 -0
- package/README.md +1 -1
- package/dist/args.js +2 -2
- package/dist/builder/buildPlanner.js +71 -0
- package/dist/builder/configGenerator.js +9 -7
- package/dist/builder/dependencyInstaller.js +0 -1
- package/dist/builder/fileProcessor.js +47 -44
- package/dist/builder/gitInitializer.js +20 -1
- package/dist/builder/index.js +19 -38
- package/dist/builder/moduleSelector.js +0 -1
- package/dist/builder/templateContext.js +0 -22
- package/dist/index.js +4 -3
- package/dist/modules/registry.js +4 -6
- package/dist/output/nextSteps.js +0 -1
- package/dist/prompts/index.js +0 -1
- package/dist/prompts/stack.js +4 -1
- package/dist/types/index.js +7 -3
- package/dist/ui/index.js +0 -1
- package/dist/ui/messages.js +25 -1
- package/dist/ui/prompts.js +0 -1
- package/dist/ui/theme.js +0 -1
- package/dist/validators/index.js +10 -41
- package/package.json +2 -1
- package/templates/backend/express/.prettierrc +2 -2
- package/templates/backend/express/config.json +1 -5
- package/templates/backend/express/eslint.config.mjs +40 -40
- package/templates/backend/express/gitignore +36 -36
- package/templates/backend/express/src/{index.ts → index.ts.hbs} +28 -29
- package/templates/backend/express/src/routes/health.ts +12 -12
- package/templates/backend/express/tsconfig.json +25 -25
- package/templates/backend/nestjs/.prettierrc +16 -16
- package/templates/backend/nestjs/config.json +1 -4
- package/templates/backend/nestjs/eslint.config.mjs +40 -40
- package/templates/backend/nestjs/gitignore +36 -36
- package/templates/backend/nestjs/nest-cli.json +9 -9
- package/templates/backend/nestjs/src/app.module.ts.hbs +34 -34
- package/templates/backend/nestjs/src/health/health.controller.ts +12 -12
- package/templates/backend/nestjs/src/main.ts +23 -23
- package/templates/backend/nestjs/tsconfig.json +25 -25
- package/templates/frontend/nextjs/components.json +19 -19
- package/templates/frontend/nextjs/config.json +1 -2
- package/templates/frontend/nextjs/gitignore +42 -42
- package/templates/frontend/nextjs/lib/utils.ts +6 -6
- package/templates/frontend/nextjs/proxy.ts.hbs +24 -24
- package/templates/frontend/nextjs/tsconfig.json +34 -34
- package/templates/frontend/tanstack/config.json +1 -4
- package/templates/frontend/tanstack/src/routeTree.gen.ts +68 -68
- package/templates/frontend/tanstack/src/styles/app.css +85 -85
- package/templates/modules/auth/express/config.json +1 -8
- package/templates/modules/auth/express/src/controllers/authController.ts.hbs +116 -116
- package/templates/modules/auth/express/src/middleware/requireAuth.ts.hbs +26 -26
- package/templates/modules/auth/express/src/services/userStore.ts.hbs +117 -117
- package/templates/modules/auth/nestjs/config.json +1 -5
- package/templates/modules/auth/nestjs/src/auth/auth.controller.ts +46 -46
- package/templates/modules/auth/nestjs/src/auth/auth.guard.ts +36 -36
- package/templates/modules/auth/nestjs/src/auth/auth.module.ts.hbs +22 -22
- package/templates/modules/auth/nestjs/src/auth/auth.schemas.ts +35 -35
- package/templates/modules/auth/nestjs/src/auth/auth.service.ts +70 -70
- package/templates/modules/auth/nestjs/src/auth/jwt.ts +27 -27
- package/templates/modules/auth/nestjs/src/auth/users.service.ts.hbs +144 -144
- package/templates/modules/auth/nestjs/src/auth/zod-validation.pipe.ts +16 -16
- package/templates/modules/auth/nextjs/app/dashboard/page.tsx +96 -96
- package/templates/modules/auth/nextjs/app/sign-in/page.tsx +35 -35
- package/templates/modules/auth/nextjs/app/sign-up/page.tsx +35 -35
- package/templates/modules/auth/nextjs/components/auth/sign-in-form.tsx +132 -132
- package/templates/modules/auth/nextjs/components/auth/sign-out-button.tsx +50 -50
- package/templates/modules/auth/nextjs/components/auth/sign-up-form.tsx +152 -152
- package/templates/modules/auth/nextjs/config.json +2 -15
- package/templates/modules/auth/nextjs/lib/auth-schema.ts +39 -39
- package/templates/modules/auth/nextjs/lib/auth.ts.hbs +37 -37
- package/templates/modules/auth/tanstack/config.json +2 -16
- package/templates/modules/auth/tanstack/src/lib/auth-functions.ts +4 -4
- package/templates/modules/auth/tanstack/src/routeTree.gen.ts +140 -140
- package/templates/modules/docker/.dockerignore +14 -14
- package/templates/modules/docker/Dockerfile.hbs +1 -3
- package/templates/modules/docker/config.json +1 -5
- package/templates/modules/docker/docker-compose.yml.hbs +9 -9
- package/templates/modules/drizzle/express/config.json +1 -2
- package/templates/modules/drizzle/express/drizzle.config.ts +13 -13
- package/templates/modules/drizzle/express/src/lib/db.ts +13 -13
- package/templates/modules/drizzle/express/src/lib/schema.ts +10 -10
- package/templates/modules/drizzle/nestjs/config.json +1 -2
- package/templates/modules/drizzle/nestjs/drizzle.config.ts +13 -13
- package/templates/modules/drizzle/nestjs/src/database/database.module.ts +30 -30
- package/templates/modules/drizzle/nestjs/src/database/schema.ts +10 -10
- package/templates/modules/drizzle/nextjs/config.json +1 -4
- package/templates/modules/drizzle/nextjs/lib/db.ts +13 -13
- package/templates/modules/drizzle/nextjs/lib/schema.ts.hbs +84 -84
- package/templates/modules/drizzle/tanstack/config.json +5 -5
- package/templates/modules/mongoose/express/config.json +1 -2
- package/templates/modules/mongoose/nestjs/config.json +1 -2
- package/templates/modules/mongoose/nestjs/src/users/user.schema.ts +21 -21
- package/templates/modules/mongoose/nextjs/config.json +1 -2
- package/templates/modules/mongoose/nextjs/lib/db.ts +36 -36
- package/templates/modules/mongoose/tanstack/config.json +1 -2
- package/templates/modules/mongoose/tanstack/src/lib/db.ts +37 -37
- package/templates/modules/prisma/express/config.json +0 -1
- package/templates/modules/prisma/express/prisma/schema.prisma +1 -1
- package/templates/modules/prisma/express/prisma.config.ts +12 -12
- package/templates/modules/prisma/express/src/lib/prisma.ts +24 -24
- package/templates/modules/prisma/nestjs/config.json +0 -1
- package/templates/modules/prisma/nestjs/prisma/schema.prisma +23 -23
- package/templates/modules/prisma/nestjs/prisma.config.ts +12 -12
- package/templates/modules/prisma/nestjs/src/prisma/prisma.module.ts +11 -11
- package/templates/modules/prisma/nestjs/src/prisma/prisma.service.ts +17 -17
- package/templates/modules/prisma/nextjs/config.json +0 -1
- package/templates/modules/prisma/nextjs/prisma/schema.prisma.hbs +91 -91
- package/templates/modules/prisma/tanstack/config.json +0 -1
- package/dist/args.d.ts +0 -12
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js.map +0 -1
- package/dist/builder/configGenerator.d.ts +0 -7
- package/dist/builder/configGenerator.d.ts.map +0 -1
- package/dist/builder/configGenerator.js.map +0 -1
- package/dist/builder/dependencyInstaller.d.ts +0 -3
- package/dist/builder/dependencyInstaller.d.ts.map +0 -1
- package/dist/builder/dependencyInstaller.js.map +0 -1
- package/dist/builder/fileProcessor.d.ts +0 -6
- package/dist/builder/fileProcessor.d.ts.map +0 -1
- package/dist/builder/fileProcessor.js.map +0 -1
- package/dist/builder/gitInitializer.d.ts +0 -2
- package/dist/builder/gitInitializer.d.ts.map +0 -1
- package/dist/builder/gitInitializer.js.map +0 -1
- package/dist/builder/index.d.ts +0 -3
- package/dist/builder/index.d.ts.map +0 -1
- package/dist/builder/index.js.map +0 -1
- package/dist/builder/moduleSelector.d.ts +0 -6
- package/dist/builder/moduleSelector.d.ts.map +0 -1
- package/dist/builder/moduleSelector.js.map +0 -1
- package/dist/builder/templateContext.d.ts +0 -6
- package/dist/builder/templateContext.d.ts.map +0 -1
- package/dist/builder/templateContext.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/modules/registry.d.ts +0 -6
- package/dist/modules/registry.d.ts.map +0 -1
- package/dist/modules/registry.js.map +0 -1
- package/dist/output/nextSteps.d.ts +0 -3
- package/dist/output/nextSteps.d.ts.map +0 -1
- package/dist/output/nextSteps.js.map +0 -1
- package/dist/prompts/index.d.ts +0 -4
- package/dist/prompts/index.d.ts.map +0 -1
- package/dist/prompts/index.js.map +0 -1
- package/dist/prompts/stack.d.ts +0 -4
- package/dist/prompts/stack.d.ts.map +0 -1
- package/dist/prompts/stack.js.map +0 -1
- package/dist/types/index.d.ts +0 -137
- package/dist/types/index.d.ts.map +0 -1
- package/dist/types/index.js.map +0 -1
- package/dist/ui/index.d.ts +0 -3
- package/dist/ui/index.d.ts.map +0 -1
- package/dist/ui/index.js.map +0 -1
- package/dist/ui/messages.d.ts +0 -9
- package/dist/ui/messages.d.ts.map +0 -1
- package/dist/ui/messages.js.map +0 -1
- package/dist/ui/prompts.d.ts +0 -20
- package/dist/ui/prompts.d.ts.map +0 -1
- package/dist/ui/prompts.js.map +0 -1
- package/dist/ui/theme.d.ts +0 -9
- package/dist/ui/theme.d.ts.map +0 -1
- package/dist/ui/theme.js.map +0 -1
- package/dist/validators/index.d.ts +0 -6
- package/dist/validators/index.d.ts.map +0 -1
- package/dist/validators/index.js.map +0 -1
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
|
|
3
|
-
// @ts-nocheck
|
|
4
|
-
|
|
5
|
-
// noinspection JSUnusedGlobalSymbols
|
|
6
|
-
|
|
7
|
-
// This file was automatically generated by TanStack Router.
|
|
8
|
-
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
-
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
-
|
|
11
|
-
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
-
import { Route as IndexRouteImport } from './routes/index'
|
|
13
|
-
|
|
14
|
-
const IndexRoute = IndexRouteImport.update({
|
|
15
|
-
id: '/',
|
|
16
|
-
path: '/',
|
|
17
|
-
getParentRoute: () => rootRouteImport,
|
|
18
|
-
} as any)
|
|
19
|
-
|
|
20
|
-
export interface FileRoutesByFullPath {
|
|
21
|
-
'/': typeof IndexRoute
|
|
22
|
-
}
|
|
23
|
-
export interface FileRoutesByTo {
|
|
24
|
-
'/': typeof IndexRoute
|
|
25
|
-
}
|
|
26
|
-
export interface FileRoutesById {
|
|
27
|
-
__root__: typeof rootRouteImport
|
|
28
|
-
'/': typeof IndexRoute
|
|
29
|
-
}
|
|
30
|
-
export interface FileRouteTypes {
|
|
31
|
-
fileRoutesByFullPath: FileRoutesByFullPath
|
|
32
|
-
fullPaths: '/'
|
|
33
|
-
fileRoutesByTo: FileRoutesByTo
|
|
34
|
-
to: '/'
|
|
35
|
-
id: '__root__' | '/'
|
|
36
|
-
fileRoutesById: FileRoutesById
|
|
37
|
-
}
|
|
38
|
-
export interface RootRouteChildren {
|
|
39
|
-
IndexRoute: typeof IndexRoute
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
declare module '@tanstack/react-router' {
|
|
43
|
-
interface FileRoutesByPath {
|
|
44
|
-
'/': {
|
|
45
|
-
id: '/'
|
|
46
|
-
path: '/'
|
|
47
|
-
fullPath: '/'
|
|
48
|
-
preLoaderRoute: typeof IndexRouteImport
|
|
49
|
-
parentRoute: typeof rootRouteImport
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const rootRouteChildren: RootRouteChildren = {
|
|
55
|
-
IndexRoute: IndexRoute,
|
|
56
|
-
}
|
|
57
|
-
export const routeTree = rootRouteImport
|
|
58
|
-
._addFileChildren(rootRouteChildren)
|
|
59
|
-
._addFileTypes<FileRouteTypes>()
|
|
60
|
-
|
|
61
|
-
import type { getRouter } from './router.tsx'
|
|
62
|
-
import type { createStart } from '@tanstack/react-start'
|
|
63
|
-
declare module '@tanstack/react-start' {
|
|
64
|
-
interface Register {
|
|
65
|
-
ssr: true
|
|
66
|
-
router: Awaited<ReturnType<typeof getRouter>>
|
|
67
|
-
}
|
|
68
|
-
}
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
// noinspection JSUnusedGlobalSymbols
|
|
6
|
+
|
|
7
|
+
// This file was automatically generated by TanStack Router.
|
|
8
|
+
// You should NOT make any changes in this file as it will be overwritten.
|
|
9
|
+
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
|
10
|
+
|
|
11
|
+
import { Route as rootRouteImport } from './routes/__root'
|
|
12
|
+
import { Route as IndexRouteImport } from './routes/index'
|
|
13
|
+
|
|
14
|
+
const IndexRoute = IndexRouteImport.update({
|
|
15
|
+
id: '/',
|
|
16
|
+
path: '/',
|
|
17
|
+
getParentRoute: () => rootRouteImport,
|
|
18
|
+
} as any)
|
|
19
|
+
|
|
20
|
+
export interface FileRoutesByFullPath {
|
|
21
|
+
'/': typeof IndexRoute
|
|
22
|
+
}
|
|
23
|
+
export interface FileRoutesByTo {
|
|
24
|
+
'/': typeof IndexRoute
|
|
25
|
+
}
|
|
26
|
+
export interface FileRoutesById {
|
|
27
|
+
__root__: typeof rootRouteImport
|
|
28
|
+
'/': typeof IndexRoute
|
|
29
|
+
}
|
|
30
|
+
export interface FileRouteTypes {
|
|
31
|
+
fileRoutesByFullPath: FileRoutesByFullPath
|
|
32
|
+
fullPaths: '/'
|
|
33
|
+
fileRoutesByTo: FileRoutesByTo
|
|
34
|
+
to: '/'
|
|
35
|
+
id: '__root__' | '/'
|
|
36
|
+
fileRoutesById: FileRoutesById
|
|
37
|
+
}
|
|
38
|
+
export interface RootRouteChildren {
|
|
39
|
+
IndexRoute: typeof IndexRoute
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
declare module '@tanstack/react-router' {
|
|
43
|
+
interface FileRoutesByPath {
|
|
44
|
+
'/': {
|
|
45
|
+
id: '/'
|
|
46
|
+
path: '/'
|
|
47
|
+
fullPath: '/'
|
|
48
|
+
preLoaderRoute: typeof IndexRouteImport
|
|
49
|
+
parentRoute: typeof rootRouteImport
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rootRouteChildren: RootRouteChildren = {
|
|
55
|
+
IndexRoute: IndexRoute,
|
|
56
|
+
}
|
|
57
|
+
export const routeTree = rootRouteImport
|
|
58
|
+
._addFileChildren(rootRouteChildren)
|
|
59
|
+
._addFileTypes<FileRouteTypes>()
|
|
60
|
+
|
|
61
|
+
import type { getRouter } from './router.tsx'
|
|
62
|
+
import type { createStart } from '@tanstack/react-start'
|
|
63
|
+
declare module '@tanstack/react-start' {
|
|
64
|
+
interface Register {
|
|
65
|
+
ssr: true
|
|
66
|
+
router: Awaited<ReturnType<typeof getRouter>>
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -1,85 +1,85 @@
|
|
|
1
|
-
@import "tailwindcss" source("../");
|
|
2
|
-
|
|
3
|
-
@theme inline {
|
|
4
|
-
--color-background: var(--background);
|
|
5
|
-
--color-foreground: var(--foreground);
|
|
6
|
-
--color-card: var(--card);
|
|
7
|
-
--color-card-foreground: var(--card-foreground);
|
|
8
|
-
--color-primary: var(--primary);
|
|
9
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
10
|
-
--color-secondary: var(--secondary);
|
|
11
|
-
--color-secondary-foreground: var(--secondary-foreground);
|
|
12
|
-
--color-muted: var(--muted);
|
|
13
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
14
|
-
--color-accent: var(--accent);
|
|
15
|
-
--color-accent-foreground: var(--accent-foreground);
|
|
16
|
-
--color-destructive: var(--destructive);
|
|
17
|
-
--color-border: var(--border);
|
|
18
|
-
--color-input: var(--input);
|
|
19
|
-
--color-ring: var(--ring);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
:root {
|
|
23
|
-
--background: oklch(0.985 0 0);
|
|
24
|
-
--foreground: oklch(0.145 0 0);
|
|
25
|
-
--card: oklch(1 0 0);
|
|
26
|
-
--card-foreground: oklch(0.145 0 0);
|
|
27
|
-
--primary: oklch(0.205 0 0);
|
|
28
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
29
|
-
--secondary: oklch(0.97 0 0);
|
|
30
|
-
--secondary-foreground: oklch(0.205 0 0);
|
|
31
|
-
--muted: oklch(0.97 0 0);
|
|
32
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
33
|
-
--accent: oklch(0.97 0 0);
|
|
34
|
-
--accent-foreground: oklch(0.205 0 0);
|
|
35
|
-
--destructive: oklch(0.577 0.245 27.325);
|
|
36
|
-
--border: oklch(0.922 0 0);
|
|
37
|
-
--input: oklch(0.922 0 0);
|
|
38
|
-
--ring: oklch(0.708 0 0);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@media (prefers-color-scheme: dark) {
|
|
42
|
-
:root {
|
|
43
|
-
--background: oklch(0.145 0 0);
|
|
44
|
-
--foreground: oklch(0.985 0 0);
|
|
45
|
-
--card: oklch(0.205 0 0);
|
|
46
|
-
--card-foreground: oklch(0.985 0 0);
|
|
47
|
-
--primary: oklch(0.922 0 0);
|
|
48
|
-
--primary-foreground: oklch(0.205 0 0);
|
|
49
|
-
--secondary: oklch(0.269 0 0);
|
|
50
|
-
--secondary-foreground: oklch(0.985 0 0);
|
|
51
|
-
--muted: oklch(0.269 0 0);
|
|
52
|
-
--muted-foreground: oklch(0.708 0 0);
|
|
53
|
-
--accent: oklch(0.269 0 0);
|
|
54
|
-
--accent-foreground: oklch(0.985 0 0);
|
|
55
|
-
--destructive: oklch(0.704 0.191 22.216);
|
|
56
|
-
--border: oklch(1 0 0 / 10%);
|
|
57
|
-
--input: oklch(1 0 0 / 15%);
|
|
58
|
-
--ring: oklch(0.556 0 0);
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
@layer base {
|
|
63
|
-
*,
|
|
64
|
-
::after,
|
|
65
|
-
::before,
|
|
66
|
-
::backdrop,
|
|
67
|
-
::file-selector-button {
|
|
68
|
-
border-color: var(--color-gray-200, currentcolor);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
@layer base {
|
|
73
|
-
html {
|
|
74
|
-
color-scheme: light dark;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
* {
|
|
78
|
-
@apply border-gray-200 dark:border-gray-800;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
html,
|
|
82
|
-
body {
|
|
83
|
-
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
|
|
84
|
-
}
|
|
85
|
-
}
|
|
1
|
+
@import "tailwindcss" source("../");
|
|
2
|
+
|
|
3
|
+
@theme inline {
|
|
4
|
+
--color-background: var(--background);
|
|
5
|
+
--color-foreground: var(--foreground);
|
|
6
|
+
--color-card: var(--card);
|
|
7
|
+
--color-card-foreground: var(--card-foreground);
|
|
8
|
+
--color-primary: var(--primary);
|
|
9
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
10
|
+
--color-secondary: var(--secondary);
|
|
11
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
12
|
+
--color-muted: var(--muted);
|
|
13
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
14
|
+
--color-accent: var(--accent);
|
|
15
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
16
|
+
--color-destructive: var(--destructive);
|
|
17
|
+
--color-border: var(--border);
|
|
18
|
+
--color-input: var(--input);
|
|
19
|
+
--color-ring: var(--ring);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
:root {
|
|
23
|
+
--background: oklch(0.985 0 0);
|
|
24
|
+
--foreground: oklch(0.145 0 0);
|
|
25
|
+
--card: oklch(1 0 0);
|
|
26
|
+
--card-foreground: oklch(0.145 0 0);
|
|
27
|
+
--primary: oklch(0.205 0 0);
|
|
28
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
29
|
+
--secondary: oklch(0.97 0 0);
|
|
30
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
31
|
+
--muted: oklch(0.97 0 0);
|
|
32
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
33
|
+
--accent: oklch(0.97 0 0);
|
|
34
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
35
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
36
|
+
--border: oklch(0.922 0 0);
|
|
37
|
+
--input: oklch(0.922 0 0);
|
|
38
|
+
--ring: oklch(0.708 0 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@media (prefers-color-scheme: dark) {
|
|
42
|
+
:root {
|
|
43
|
+
--background: oklch(0.145 0 0);
|
|
44
|
+
--foreground: oklch(0.985 0 0);
|
|
45
|
+
--card: oklch(0.205 0 0);
|
|
46
|
+
--card-foreground: oklch(0.985 0 0);
|
|
47
|
+
--primary: oklch(0.922 0 0);
|
|
48
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
49
|
+
--secondary: oklch(0.269 0 0);
|
|
50
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
51
|
+
--muted: oklch(0.269 0 0);
|
|
52
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
53
|
+
--accent: oklch(0.269 0 0);
|
|
54
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
55
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
56
|
+
--border: oklch(1 0 0 / 10%);
|
|
57
|
+
--input: oklch(1 0 0 / 15%);
|
|
58
|
+
--ring: oklch(0.556 0 0);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@layer base {
|
|
63
|
+
*,
|
|
64
|
+
::after,
|
|
65
|
+
::before,
|
|
66
|
+
::backdrop,
|
|
67
|
+
::file-selector-button {
|
|
68
|
+
border-color: var(--color-gray-200, currentcolor);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@layer base {
|
|
73
|
+
html {
|
|
74
|
+
color-scheme: light dark;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
* {
|
|
78
|
+
@apply border-gray-200 dark:border-gray-800;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
html,
|
|
82
|
+
body {
|
|
83
|
+
@apply bg-gray-50 text-gray-900 dark:bg-gray-950 dark:text-gray-200;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -16,12 +16,5 @@
|
|
|
16
16
|
"envVars": {
|
|
17
17
|
"JWT_SECRET": "please-change-me",
|
|
18
18
|
"JWT_EXPIRES_IN": "1h"
|
|
19
|
-
}
|
|
20
|
-
"templateFiles": [
|
|
21
|
-
"src/lib/jwt.ts.hbs",
|
|
22
|
-
"src/middleware/requireAuth.ts.hbs",
|
|
23
|
-
"src/services/userStore.ts.hbs",
|
|
24
|
-
"src/routes/auth.ts.hbs",
|
|
25
|
-
"src/controllers/authController.ts.hbs"
|
|
26
|
-
]
|
|
19
|
+
}
|
|
27
20
|
}
|
|
@@ -1,116 +1,116 @@
|
|
|
1
|
-
import { Request, Response } from "express";
|
|
2
|
-
import bcrypt from "bcrypt";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
import { signToken } from "../lib/jwt";
|
|
5
|
-
import { AuthenticatedRequest } from "../middleware/requireAuth";
|
|
6
|
-
import {
|
|
7
|
-
createUser,
|
|
8
|
-
findUserByEmail,
|
|
9
|
-
isDuplicateEmailError,
|
|
10
|
-
toPublicUser,
|
|
11
|
-
} from "../services/userStore";
|
|
12
|
-
|
|
13
|
-
const SALT_ROUNDS = 10;
|
|
14
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
15
|
-
// bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
|
|
16
|
-
const MAX_PASSWORD_BYTES = 72;
|
|
17
|
-
|
|
18
|
-
// Emails are trimmed and lowercased so sign-in is case-insensitive
|
|
19
|
-
const emailSchema = z
|
|
20
|
-
.string("Email and password are required")
|
|
21
|
-
.trim()
|
|
22
|
-
.toLowerCase()
|
|
23
|
-
.pipe(z.email("Email must be a valid email address"));
|
|
24
|
-
|
|
25
|
-
const registerSchema = z.object({
|
|
26
|
-
email: emailSchema,
|
|
27
|
-
password: z
|
|
28
|
-
.string("Email and password are required")
|
|
29
|
-
.min(
|
|
30
|
-
MIN_PASSWORD_LENGTH,
|
|
31
|
-
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`,
|
|
32
|
-
)
|
|
33
|
-
.refine(
|
|
34
|
-
(password) => Buffer.byteLength(password, "utf8") <= MAX_PASSWORD_BYTES,
|
|
35
|
-
`Password must be at most ${MAX_PASSWORD_BYTES} bytes`,
|
|
36
|
-
),
|
|
37
|
-
name: z.string("Name must be a string").nullish(),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const loginSchema = z.object({
|
|
41
|
-
email: emailSchema,
|
|
42
|
-
password: z.string("Email and password are required").min(1),
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
// Sends a 400 with the first validation message and returns null when the body is invalid
|
|
46
|
-
function parseBody<T>(schema: z.ZodType<T>, req: Request, res: Response): T | null {
|
|
47
|
-
// Express 5 leaves req.body undefined when the request has no body
|
|
48
|
-
const result = schema.safeParse(req.body ?? {});
|
|
49
|
-
if (result.success) return result.data;
|
|
50
|
-
|
|
51
|
-
res.status(400).json({ error: result.error.issues[0].message });
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export async function register(req: Request, res: Response): Promise<void> {
|
|
56
|
-
const body = parseBody(registerSchema, req, res);
|
|
57
|
-
if (!body) return;
|
|
58
|
-
const { email, password, name = null } = body;
|
|
59
|
-
|
|
60
|
-
const existingUser = await findUserByEmail(email);
|
|
61
|
-
if (existingUser) {
|
|
62
|
-
res.status(409).json({ error: "User already exists" });
|
|
63
|
-
return;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
|
67
|
-
|
|
68
|
-
let user;
|
|
69
|
-
try {
|
|
70
|
-
user = await createUser({ email, name, password: hashedPassword });
|
|
71
|
-
} catch (error) {
|
|
72
|
-
// A concurrent request registered the same email after the check above
|
|
73
|
-
if (isDuplicateEmailError(error)) {
|
|
74
|
-
res.status(409).json({ error: "User already exists" });
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
throw error;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
81
|
-
|
|
82
|
-
res.status(201).json({ user: toPublicUser(user), token });
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export async function login(req: Request, res: Response): Promise<void> {
|
|
86
|
-
const body = parseBody(loginSchema, req, res);
|
|
87
|
-
if (!body) return;
|
|
88
|
-
const { email, password } = body;
|
|
89
|
-
|
|
90
|
-
const user = await findUserByEmail(email);
|
|
91
|
-
|
|
92
|
-
if (!user) {
|
|
93
|
-
res.status(401).json({ error: "Invalid credentials" });
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
98
|
-
|
|
99
|
-
if (!isValidPassword) {
|
|
100
|
-
res.status(401).json({ error: "Invalid credentials" });
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
105
|
-
|
|
106
|
-
res.json({ user: toPublicUser(user), token });
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function me(req: AuthenticatedRequest, res: Response): void {
|
|
110
|
-
if (!req.user) {
|
|
111
|
-
res.status(401).json({ error: "Unauthorized" });
|
|
112
|
-
return;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
res.json({ user: req.user });
|
|
116
|
-
}
|
|
1
|
+
import { Request, Response } from "express";
|
|
2
|
+
import bcrypt from "bcrypt";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { signToken } from "../lib/jwt";
|
|
5
|
+
import { AuthenticatedRequest } from "../middleware/requireAuth";
|
|
6
|
+
import {
|
|
7
|
+
createUser,
|
|
8
|
+
findUserByEmail,
|
|
9
|
+
isDuplicateEmailError,
|
|
10
|
+
toPublicUser,
|
|
11
|
+
} from "../services/userStore";
|
|
12
|
+
|
|
13
|
+
const SALT_ROUNDS = 10;
|
|
14
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
15
|
+
// bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
|
|
16
|
+
const MAX_PASSWORD_BYTES = 72;
|
|
17
|
+
|
|
18
|
+
// Emails are trimmed and lowercased so sign-in is case-insensitive
|
|
19
|
+
const emailSchema = z
|
|
20
|
+
.string("Email and password are required")
|
|
21
|
+
.trim()
|
|
22
|
+
.toLowerCase()
|
|
23
|
+
.pipe(z.email("Email must be a valid email address"));
|
|
24
|
+
|
|
25
|
+
const registerSchema = z.object({
|
|
26
|
+
email: emailSchema,
|
|
27
|
+
password: z
|
|
28
|
+
.string("Email and password are required")
|
|
29
|
+
.min(
|
|
30
|
+
MIN_PASSWORD_LENGTH,
|
|
31
|
+
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`,
|
|
32
|
+
)
|
|
33
|
+
.refine(
|
|
34
|
+
(password) => Buffer.byteLength(password, "utf8") <= MAX_PASSWORD_BYTES,
|
|
35
|
+
`Password must be at most ${MAX_PASSWORD_BYTES} bytes`,
|
|
36
|
+
),
|
|
37
|
+
name: z.string("Name must be a string").nullish(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const loginSchema = z.object({
|
|
41
|
+
email: emailSchema,
|
|
42
|
+
password: z.string("Email and password are required").min(1),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Sends a 400 with the first validation message and returns null when the body is invalid
|
|
46
|
+
function parseBody<T>(schema: z.ZodType<T>, req: Request, res: Response): T | null {
|
|
47
|
+
// Express 5 leaves req.body undefined when the request has no body
|
|
48
|
+
const result = schema.safeParse(req.body ?? {});
|
|
49
|
+
if (result.success) return result.data;
|
|
50
|
+
|
|
51
|
+
res.status(400).json({ error: result.error.issues[0].message });
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function register(req: Request, res: Response): Promise<void> {
|
|
56
|
+
const body = parseBody(registerSchema, req, res);
|
|
57
|
+
if (!body) return;
|
|
58
|
+
const { email, password, name = null } = body;
|
|
59
|
+
|
|
60
|
+
const existingUser = await findUserByEmail(email);
|
|
61
|
+
if (existingUser) {
|
|
62
|
+
res.status(409).json({ error: "User already exists" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);
|
|
67
|
+
|
|
68
|
+
let user;
|
|
69
|
+
try {
|
|
70
|
+
user = await createUser({ email, name, password: hashedPassword });
|
|
71
|
+
} catch (error) {
|
|
72
|
+
// A concurrent request registered the same email after the check above
|
|
73
|
+
if (isDuplicateEmailError(error)) {
|
|
74
|
+
res.status(409).json({ error: "User already exists" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
throw error;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
81
|
+
|
|
82
|
+
res.status(201).json({ user: toPublicUser(user), token });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function login(req: Request, res: Response): Promise<void> {
|
|
86
|
+
const body = parseBody(loginSchema, req, res);
|
|
87
|
+
if (!body) return;
|
|
88
|
+
const { email, password } = body;
|
|
89
|
+
|
|
90
|
+
const user = await findUserByEmail(email);
|
|
91
|
+
|
|
92
|
+
if (!user) {
|
|
93
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const isValidPassword = await bcrypt.compare(password, user.password);
|
|
98
|
+
|
|
99
|
+
if (!isValidPassword) {
|
|
100
|
+
res.status(401).json({ error: "Invalid credentials" });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const token = signToken({ sub: user.id, email: user.email, name: user.name });
|
|
105
|
+
|
|
106
|
+
res.json({ user: toPublicUser(user), token });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function me(req: AuthenticatedRequest, res: Response): void {
|
|
110
|
+
if (!req.user) {
|
|
111
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
res.json({ user: req.user });
|
|
116
|
+
}
|
|
@@ -1,26 +1,26 @@
|
|
|
1
|
-
import { NextFunction, Request, Response } from 'express';
|
|
2
|
-
import { AuthTokenPayload, verifyToken } from '../lib/jwt';
|
|
3
|
-
|
|
4
|
-
export interface AuthenticatedRequest extends Request {
|
|
5
|
-
user?: AuthTokenPayload;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
|
|
9
|
-
const authHeader = req.headers.authorization;
|
|
10
|
-
|
|
11
|
-
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
12
|
-
res.status(401).json({ error: 'Authorization header missing or invalid' });
|
|
13
|
-
return;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const token = authHeader.substring('Bearer '.length);
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const payload = verifyToken(token);
|
|
20
|
-
req.user = payload;
|
|
21
|
-
next();
|
|
22
|
-
} catch (error) {
|
|
23
|
-
console.error('JWT verification failed:', error);
|
|
24
|
-
res.status(401).json({ error: 'Invalid or expired token' });
|
|
25
|
-
}
|
|
26
|
-
}
|
|
1
|
+
import { NextFunction, Request, Response } from 'express';
|
|
2
|
+
import { AuthTokenPayload, verifyToken } from '../lib/jwt';
|
|
3
|
+
|
|
4
|
+
export interface AuthenticatedRequest extends Request {
|
|
5
|
+
user?: AuthTokenPayload;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function requireAuth(req: AuthenticatedRequest, res: Response, next: NextFunction): void {
|
|
9
|
+
const authHeader = req.headers.authorization;
|
|
10
|
+
|
|
11
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
12
|
+
res.status(401).json({ error: 'Authorization header missing or invalid' });
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const token = authHeader.substring('Bearer '.length);
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const payload = verifyToken(token);
|
|
20
|
+
req.user = payload;
|
|
21
|
+
next();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('JWT verification failed:', error);
|
|
24
|
+
res.status(401).json({ error: 'Invalid or expired token' });
|
|
25
|
+
}
|
|
26
|
+
}
|