better-ts-stack 2.0.0 → 2.0.2
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 +4 -3
- package/dist/prompts/index.js +0 -1
- package/dist/prompts/stack.js +4 -1
- package/dist/types/index.js +8 -4
- 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 +45 -48
- 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/lib/auth-schema.ts +38 -38
- 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 +12 -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 +12 -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 +90 -84
- package/templates/modules/drizzle/tanstack/config.json +5 -5
- package/templates/modules/drizzle/tanstack/src/lib/schema.ts.hbs +90 -84
- 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,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
|
+
}
|
|
@@ -1,117 +1,117 @@
|
|
|
1
|
-
{{#if (eq database 'drizzle')}}
|
|
2
|
-
import { eq } from "drizzle-orm";
|
|
3
|
-
import { db } from "../lib/db";
|
|
4
|
-
import { users } from "../lib/schema";
|
|
5
|
-
{{/if}}
|
|
6
|
-
{{#if (eq database 'none')}}
|
|
7
|
-
import crypto from 'crypto';
|
|
8
|
-
{{/if}}
|
|
9
|
-
{{#if (eq database 'prisma')}}
|
|
10
|
-
import { prisma } from '../lib/prisma';
|
|
11
|
-
{{/if}}
|
|
12
|
-
{{#if (eq database 'mongoose')}}
|
|
13
|
-
import UserModel from '../models/User';
|
|
14
|
-
{{/if}}
|
|
15
|
-
|
|
16
|
-
export interface AuthUser {
|
|
17
|
-
id: string;
|
|
18
|
-
email: string;
|
|
19
|
-
name?: string | null;
|
|
20
|
-
password: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface CreateUserInput {
|
|
24
|
-
email: string;
|
|
25
|
-
name?: string | null;
|
|
26
|
-
password: string;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
{{#if (eq database 'none')}}
|
|
30
|
-
const users: AuthUser[] = [];
|
|
31
|
-
{{/if}}
|
|
32
|
-
|
|
33
|
-
export {{#unless (eq database 'none')}}async {{/unless}}function findUserByEmail(email: string): Promise<AuthUser | null> {
|
|
34
|
-
{{#if (eq database 'drizzle')}}
|
|
35
|
-
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
|
36
|
-
return user ?? null;
|
|
37
|
-
{{/if}}
|
|
38
|
-
{{#if (eq database 'prisma')}}
|
|
39
|
-
return await prisma.user.findUnique({ where: { email } });
|
|
40
|
-
{{/if}}
|
|
41
|
-
|
|
42
|
-
{{#if (eq database 'mongoose')}}
|
|
43
|
-
const user = await UserModel.findOne({ email }).exec();
|
|
44
|
-
if (!user) return null;
|
|
45
|
-
return {
|
|
46
|
-
id: String(user._id),
|
|
47
|
-
email: user.email,
|
|
48
|
-
name: user.name,
|
|
49
|
-
password: user.password,
|
|
50
|
-
};
|
|
51
|
-
{{/if}}
|
|
52
|
-
|
|
53
|
-
{{#if (eq database 'none')}}
|
|
54
|
-
const user = users.find((candidate) => candidate.email === email);
|
|
55
|
-
return Promise.resolve(user ?? null);
|
|
56
|
-
{{/if}}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data: CreateUserInput): Promise<AuthUser> {
|
|
60
|
-
{{#if (eq database 'drizzle')}}
|
|
61
|
-
const [user] = await db.insert(users).values(data).returning();
|
|
62
|
-
return user;
|
|
63
|
-
{{/if}}
|
|
64
|
-
{{#if (eq database 'prisma')}}
|
|
65
|
-
return await prisma.user.create({ data });
|
|
66
|
-
{{/if}}
|
|
67
|
-
|
|
68
|
-
{{#if (eq database 'mongoose')}}
|
|
69
|
-
const user = await UserModel.create(data);
|
|
70
|
-
|
|
71
|
-
return {
|
|
72
|
-
id: String(user._id),
|
|
73
|
-
email: user.email,
|
|
74
|
-
name: user.name,
|
|
75
|
-
password: user.password,
|
|
76
|
-
};
|
|
77
|
-
{{/if}}
|
|
78
|
-
|
|
79
|
-
{{#if (eq database 'none')}}
|
|
80
|
-
// Enforce unique emails like a database constraint would, so a sign-up that
|
|
81
|
-
// raced past the findUserByEmail check is rejected instead of duplicated
|
|
82
|
-
if (users.some((candidate) => candidate.email === data.email)) {
|
|
83
|
-
return Promise.reject(
|
|
84
|
-
Object.assign(new Error('User already exists'), { code: 'DUPLICATE_EMAIL' }),
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const newUser: AuthUser = {
|
|
89
|
-
id: crypto.randomUUID(),
|
|
90
|
-
email: data.email,
|
|
91
|
-
name: data.name,
|
|
92
|
-
password: data.password,
|
|
93
|
-
};
|
|
94
|
-
|
|
95
|
-
users.push(newUser);
|
|
96
|
-
return Promise.resolve(newUser);
|
|
97
|
-
{{/if}}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Unique-constraint error codes: Prisma, PostgreSQL (Drizzle wraps it in `cause`),
|
|
101
|
-
// MongoDB, and the in-memory store
|
|
102
|
-
const DUPLICATE_KEY_CODES = new Set<unknown>(['P2002', '23505', 11000, 'DUPLICATE_EMAIL']);
|
|
103
|
-
|
|
104
|
-
// True when an insert failed because the email is already taken (e.g. concurrent sign-ups)
|
|
105
|
-
export function isDuplicateEmailError(error: unknown): boolean {
|
|
106
|
-
let current = error;
|
|
107
|
-
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth++) {
|
|
108
|
-
if (DUPLICATE_KEY_CODES.has((current as { code?: unknown }).code)) return true;
|
|
109
|
-
current = (current as { cause?: unknown }).cause;
|
|
110
|
-
}
|
|
111
|
-
return false;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export function toPublicUser(user: AuthUser): Omit<AuthUser, 'password'> {
|
|
115
|
-
const { id, email, name } = user;
|
|
116
|
-
return { id, email, name };
|
|
117
|
-
}
|
|
1
|
+
{{#if (eq database 'drizzle')}}
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { db } from "../lib/db";
|
|
4
|
+
import { users } from "../lib/schema";
|
|
5
|
+
{{/if}}
|
|
6
|
+
{{#if (eq database 'none')}}
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
{{/if}}
|
|
9
|
+
{{#if (eq database 'prisma')}}
|
|
10
|
+
import { prisma } from '../lib/prisma';
|
|
11
|
+
{{/if}}
|
|
12
|
+
{{#if (eq database 'mongoose')}}
|
|
13
|
+
import UserModel from '../models/User';
|
|
14
|
+
{{/if}}
|
|
15
|
+
|
|
16
|
+
export interface AuthUser {
|
|
17
|
+
id: string;
|
|
18
|
+
email: string;
|
|
19
|
+
name?: string | null;
|
|
20
|
+
password: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface CreateUserInput {
|
|
24
|
+
email: string;
|
|
25
|
+
name?: string | null;
|
|
26
|
+
password: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
{{#if (eq database 'none')}}
|
|
30
|
+
const users: AuthUser[] = [];
|
|
31
|
+
{{/if}}
|
|
32
|
+
|
|
33
|
+
export {{#unless (eq database 'none')}}async {{/unless}}function findUserByEmail(email: string): Promise<AuthUser | null> {
|
|
34
|
+
{{#if (eq database 'drizzle')}}
|
|
35
|
+
const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1);
|
|
36
|
+
return user ?? null;
|
|
37
|
+
{{/if}}
|
|
38
|
+
{{#if (eq database 'prisma')}}
|
|
39
|
+
return await prisma.user.findUnique({ where: { email } });
|
|
40
|
+
{{/if}}
|
|
41
|
+
|
|
42
|
+
{{#if (eq database 'mongoose')}}
|
|
43
|
+
const user = await UserModel.findOne({ email }).exec();
|
|
44
|
+
if (!user) return null;
|
|
45
|
+
return {
|
|
46
|
+
id: String(user._id),
|
|
47
|
+
email: user.email,
|
|
48
|
+
name: user.name,
|
|
49
|
+
password: user.password,
|
|
50
|
+
};
|
|
51
|
+
{{/if}}
|
|
52
|
+
|
|
53
|
+
{{#if (eq database 'none')}}
|
|
54
|
+
const user = users.find((candidate) => candidate.email === email);
|
|
55
|
+
return Promise.resolve(user ?? null);
|
|
56
|
+
{{/if}}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export {{#unless (eq database 'none')}}async {{/unless}}function createUser(data: CreateUserInput): Promise<AuthUser> {
|
|
60
|
+
{{#if (eq database 'drizzle')}}
|
|
61
|
+
const [user] = await db.insert(users).values(data).returning();
|
|
62
|
+
return user;
|
|
63
|
+
{{/if}}
|
|
64
|
+
{{#if (eq database 'prisma')}}
|
|
65
|
+
return await prisma.user.create({ data });
|
|
66
|
+
{{/if}}
|
|
67
|
+
|
|
68
|
+
{{#if (eq database 'mongoose')}}
|
|
69
|
+
const user = await UserModel.create(data);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
id: String(user._id),
|
|
73
|
+
email: user.email,
|
|
74
|
+
name: user.name,
|
|
75
|
+
password: user.password,
|
|
76
|
+
};
|
|
77
|
+
{{/if}}
|
|
78
|
+
|
|
79
|
+
{{#if (eq database 'none')}}
|
|
80
|
+
// Enforce unique emails like a database constraint would, so a sign-up that
|
|
81
|
+
// raced past the findUserByEmail check is rejected instead of duplicated
|
|
82
|
+
if (users.some((candidate) => candidate.email === data.email)) {
|
|
83
|
+
return Promise.reject(
|
|
84
|
+
Object.assign(new Error('User already exists'), { code: 'DUPLICATE_EMAIL' }),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const newUser: AuthUser = {
|
|
89
|
+
id: crypto.randomUUID(),
|
|
90
|
+
email: data.email,
|
|
91
|
+
name: data.name,
|
|
92
|
+
password: data.password,
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
users.push(newUser);
|
|
96
|
+
return Promise.resolve(newUser);
|
|
97
|
+
{{/if}}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Unique-constraint error codes: Prisma, PostgreSQL (Drizzle wraps it in `cause`),
|
|
101
|
+
// MongoDB, and the in-memory store
|
|
102
|
+
const DUPLICATE_KEY_CODES = new Set<unknown>(['P2002', '23505', 11000, 'DUPLICATE_EMAIL']);
|
|
103
|
+
|
|
104
|
+
// True when an insert failed because the email is already taken (e.g. concurrent sign-ups)
|
|
105
|
+
export function isDuplicateEmailError(error: unknown): boolean {
|
|
106
|
+
let current = error;
|
|
107
|
+
for (let depth = 0; depth < 5 && current && typeof current === 'object'; depth++) {
|
|
108
|
+
if (DUPLICATE_KEY_CODES.has((current as { code?: unknown }).code)) return true;
|
|
109
|
+
current = (current as { cause?: unknown }).cause;
|
|
110
|
+
}
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function toPublicUser(user: AuthUser): Omit<AuthUser, 'password'> {
|
|
115
|
+
const { id, email, name } = user;
|
|
116
|
+
return { id, email, name };
|
|
117
|
+
}
|
|
@@ -1,46 +1,46 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Body,
|
|
3
|
-
Controller,
|
|
4
|
-
Get,
|
|
5
|
-
HttpCode,
|
|
6
|
-
Post,
|
|
7
|
-
Req,
|
|
8
|
-
UseGuards,
|
|
9
|
-
} from "@nestjs/common";
|
|
10
|
-
|
|
11
|
-
import { type AuthenticatedRequest, AuthGuard } from "./auth.guard.js";
|
|
12
|
-
import {
|
|
13
|
-
type LoginInput,
|
|
14
|
-
loginSchema,
|
|
15
|
-
type RegisterInput,
|
|
16
|
-
registerSchema,
|
|
17
|
-
} from "./auth.schemas.js";
|
|
18
|
-
import { type AuthResponse, AuthService } from "./auth.service.js";
|
|
19
|
-
import type { AuthTokenPayload } from "./jwt.js";
|
|
20
|
-
import { ZodValidationPipe } from "./zod-validation.pipe.js";
|
|
21
|
-
|
|
22
|
-
@Controller("auth")
|
|
23
|
-
export class AuthController {
|
|
24
|
-
constructor(private readonly authService: AuthService) {}
|
|
25
|
-
|
|
26
|
-
@Post("register")
|
|
27
|
-
register(
|
|
28
|
-
@Body(new ZodValidationPipe(registerSchema)) body: RegisterInput
|
|
29
|
-
): Promise<AuthResponse> {
|
|
30
|
-
return this.authService.register(body);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
@Post("login")
|
|
34
|
-
@HttpCode(200)
|
|
35
|
-
login(
|
|
36
|
-
@Body(new ZodValidationPipe(loginSchema)) body: LoginInput
|
|
37
|
-
): Promise<AuthResponse> {
|
|
38
|
-
return this.authService.login(body);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
@Get("me")
|
|
42
|
-
@UseGuards(AuthGuard)
|
|
43
|
-
me(@Req() request: AuthenticatedRequest): { user?: AuthTokenPayload } {
|
|
44
|
-
return { user: request.user };
|
|
45
|
-
}
|
|
46
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
Body,
|
|
3
|
+
Controller,
|
|
4
|
+
Get,
|
|
5
|
+
HttpCode,
|
|
6
|
+
Post,
|
|
7
|
+
Req,
|
|
8
|
+
UseGuards,
|
|
9
|
+
} from "@nestjs/common";
|
|
10
|
+
|
|
11
|
+
import { type AuthenticatedRequest, AuthGuard } from "./auth.guard.js";
|
|
12
|
+
import {
|
|
13
|
+
type LoginInput,
|
|
14
|
+
loginSchema,
|
|
15
|
+
type RegisterInput,
|
|
16
|
+
registerSchema,
|
|
17
|
+
} from "./auth.schemas.js";
|
|
18
|
+
import { type AuthResponse, AuthService } from "./auth.service.js";
|
|
19
|
+
import type { AuthTokenPayload } from "./jwt.js";
|
|
20
|
+
import { ZodValidationPipe } from "./zod-validation.pipe.js";
|
|
21
|
+
|
|
22
|
+
@Controller("auth")
|
|
23
|
+
export class AuthController {
|
|
24
|
+
constructor(private readonly authService: AuthService) {}
|
|
25
|
+
|
|
26
|
+
@Post("register")
|
|
27
|
+
register(
|
|
28
|
+
@Body(new ZodValidationPipe(registerSchema)) body: RegisterInput
|
|
29
|
+
): Promise<AuthResponse> {
|
|
30
|
+
return this.authService.register(body);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
@Post("login")
|
|
34
|
+
@HttpCode(200)
|
|
35
|
+
login(
|
|
36
|
+
@Body(new ZodValidationPipe(loginSchema)) body: LoginInput
|
|
37
|
+
): Promise<AuthResponse> {
|
|
38
|
+
return this.authService.login(body);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@Get("me")
|
|
42
|
+
@UseGuards(AuthGuard)
|
|
43
|
+
me(@Req() request: AuthenticatedRequest): { user?: AuthTokenPayload } {
|
|
44
|
+
return { user: request.user };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type CanActivate,
|
|
3
|
-
type ExecutionContext,
|
|
4
|
-
Injectable,
|
|
5
|
-
UnauthorizedException,
|
|
6
|
-
} from "@nestjs/common";
|
|
7
|
-
import type { Request } from "express";
|
|
8
|
-
|
|
9
|
-
import { type AuthTokenPayload, verifyToken } from "./jwt.js";
|
|
10
|
-
|
|
11
|
-
export interface AuthenticatedRequest extends Request {
|
|
12
|
-
user?: AuthTokenPayload;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// Requires a valid "Authorization: Bearer <token>" header and sets request.user
|
|
16
|
-
@Injectable()
|
|
17
|
-
export class AuthGuard implements CanActivate {
|
|
18
|
-
canActivate(context: ExecutionContext): boolean {
|
|
19
|
-
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
|
20
|
-
const authHeader = request.headers.authorization;
|
|
21
|
-
|
|
22
|
-
if (!authHeader?.startsWith("Bearer ")) {
|
|
23
|
-
throw new UnauthorizedException(
|
|
24
|
-
"Authorization header missing or invalid"
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
try {
|
|
29
|
-
request.user = verifyToken(authHeader.substring("Bearer ".length));
|
|
30
|
-
} catch {
|
|
31
|
-
throw new UnauthorizedException("Invalid or expired token");
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
type CanActivate,
|
|
3
|
+
type ExecutionContext,
|
|
4
|
+
Injectable,
|
|
5
|
+
UnauthorizedException,
|
|
6
|
+
} from "@nestjs/common";
|
|
7
|
+
import type { Request } from "express";
|
|
8
|
+
|
|
9
|
+
import { type AuthTokenPayload, verifyToken } from "./jwt.js";
|
|
10
|
+
|
|
11
|
+
export interface AuthenticatedRequest extends Request {
|
|
12
|
+
user?: AuthTokenPayload;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Requires a valid "Authorization: Bearer <token>" header and sets request.user
|
|
16
|
+
@Injectable()
|
|
17
|
+
export class AuthGuard implements CanActivate {
|
|
18
|
+
canActivate(context: ExecutionContext): boolean {
|
|
19
|
+
const request = context.switchToHttp().getRequest<AuthenticatedRequest>();
|
|
20
|
+
const authHeader = request.headers.authorization;
|
|
21
|
+
|
|
22
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
23
|
+
throw new UnauthorizedException(
|
|
24
|
+
"Authorization header missing or invalid"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
request.user = verifyToken(authHeader.substring("Bearer ".length));
|
|
30
|
+
} catch {
|
|
31
|
+
throw new UnauthorizedException("Invalid or expired token");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import { Module } from "@nestjs/common";
|
|
2
|
-
{{#if (eq database 'mongoose')}}
|
|
3
|
-
import { MongooseModule } from "@nestjs/mongoose";
|
|
4
|
-
{{/if}}
|
|
5
|
-
|
|
6
|
-
{{#if (eq database 'mongoose')}}
|
|
7
|
-
import { User, UserSchema } from "../users/user.schema.js";
|
|
8
|
-
{{/if}}
|
|
9
|
-
import { AuthController } from "./auth.controller.js";
|
|
10
|
-
import { AuthService } from "./auth.service.js";
|
|
11
|
-
import { UsersService } from "./users.service.js";
|
|
12
|
-
|
|
13
|
-
@Module({
|
|
14
|
-
{{#if (eq database 'mongoose')}}
|
|
15
|
-
imports: [
|
|
16
|
-
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
|
17
|
-
],
|
|
18
|
-
{{/if}}
|
|
19
|
-
controllers: [AuthController],
|
|
20
|
-
providers: [AuthService, UsersService],
|
|
21
|
-
})
|
|
22
|
-
export class AuthModule {}
|
|
1
|
+
import { Module } from "@nestjs/common";
|
|
2
|
+
{{#if (eq database 'mongoose')}}
|
|
3
|
+
import { MongooseModule } from "@nestjs/mongoose";
|
|
4
|
+
{{/if}}
|
|
5
|
+
|
|
6
|
+
{{#if (eq database 'mongoose')}}
|
|
7
|
+
import { User, UserSchema } from "../users/user.schema.js";
|
|
8
|
+
{{/if}}
|
|
9
|
+
import { AuthController } from "./auth.controller.js";
|
|
10
|
+
import { AuthService } from "./auth.service.js";
|
|
11
|
+
import { UsersService } from "./users.service.js";
|
|
12
|
+
|
|
13
|
+
@Module({
|
|
14
|
+
{{#if (eq database 'mongoose')}}
|
|
15
|
+
imports: [
|
|
16
|
+
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
|
17
|
+
],
|
|
18
|
+
{{/if}}
|
|
19
|
+
controllers: [AuthController],
|
|
20
|
+
providers: [AuthService, UsersService],
|
|
21
|
+
})
|
|
22
|
+
export class AuthModule {}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
const MIN_PASSWORD_LENGTH = 8;
|
|
4
|
-
// bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
|
|
5
|
-
const MAX_PASSWORD_BYTES = 72;
|
|
6
|
-
|
|
7
|
-
// Emails are trimmed and lowercased so sign-in is case-insensitive
|
|
8
|
-
const emailSchema = z
|
|
9
|
-
.string("Email and password are required")
|
|
10
|
-
.trim()
|
|
11
|
-
.toLowerCase()
|
|
12
|
-
.pipe(z.email("Email must be a valid email address"));
|
|
13
|
-
|
|
14
|
-
export const registerSchema = z.object({
|
|
15
|
-
email: emailSchema,
|
|
16
|
-
password: z
|
|
17
|
-
.string("Email and password are required")
|
|
18
|
-
.min(
|
|
19
|
-
MIN_PASSWORD_LENGTH,
|
|
20
|
-
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
|
21
|
-
)
|
|
22
|
-
.refine(
|
|
23
|
-
(password) => Buffer.byteLength(password, "utf8") <= MAX_PASSWORD_BYTES,
|
|
24
|
-
`Password must be at most ${MAX_PASSWORD_BYTES} bytes`
|
|
25
|
-
),
|
|
26
|
-
name: z.string("Name must be a string").nullish(),
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
export const loginSchema = z.object({
|
|
30
|
-
email: emailSchema,
|
|
31
|
-
password: z.string("Email and password are required").min(1),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
35
|
-
export type LoginInput = z.infer<typeof loginSchema>;
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const MIN_PASSWORD_LENGTH = 8;
|
|
4
|
+
// bcrypt ignores bytes beyond 72, so longer passwords would be silently truncated
|
|
5
|
+
const MAX_PASSWORD_BYTES = 72;
|
|
6
|
+
|
|
7
|
+
// Emails are trimmed and lowercased so sign-in is case-insensitive
|
|
8
|
+
const emailSchema = z
|
|
9
|
+
.string("Email and password are required")
|
|
10
|
+
.trim()
|
|
11
|
+
.toLowerCase()
|
|
12
|
+
.pipe(z.email("Email must be a valid email address"));
|
|
13
|
+
|
|
14
|
+
export const registerSchema = z.object({
|
|
15
|
+
email: emailSchema,
|
|
16
|
+
password: z
|
|
17
|
+
.string("Email and password are required")
|
|
18
|
+
.min(
|
|
19
|
+
MIN_PASSWORD_LENGTH,
|
|
20
|
+
`Password must be at least ${MIN_PASSWORD_LENGTH} characters`
|
|
21
|
+
)
|
|
22
|
+
.refine(
|
|
23
|
+
(password) => Buffer.byteLength(password, "utf8") <= MAX_PASSWORD_BYTES,
|
|
24
|
+
`Password must be at most ${MAX_PASSWORD_BYTES} bytes`
|
|
25
|
+
),
|
|
26
|
+
name: z.string("Name must be a string").nullish(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const loginSchema = z.object({
|
|
30
|
+
email: emailSchema,
|
|
31
|
+
password: z.string("Email and password are required").min(1),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export type RegisterInput = z.infer<typeof registerSchema>;
|
|
35
|
+
export type LoginInput = z.infer<typeof loginSchema>;
|