nurjs-cli 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/LICENSE +21 -0
- package/README.md +114 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +48 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/doctor.command.d.ts +2 -0
- package/dist/commands/doctor.command.d.ts.map +1 -0
- package/dist/commands/doctor.command.js +32 -0
- package/dist/commands/doctor.command.js.map +1 -0
- package/dist/commands/init.command.d.ts +6 -0
- package/dist/commands/init.command.d.ts.map +1 -0
- package/dist/commands/init.command.js +117 -0
- package/dist/commands/init.command.js.map +1 -0
- package/dist/commands/install.command.d.ts +8 -0
- package/dist/commands/install.command.d.ts.map +1 -0
- package/dist/commands/install.command.js +86 -0
- package/dist/commands/install.command.js.map +1 -0
- package/dist/commands/list.command.d.ts +2 -0
- package/dist/commands/list.command.d.ts.map +1 -0
- package/dist/commands/list.command.js +14 -0
- package/dist/commands/list.command.js.map +1 -0
- package/dist/commands/start.command.d.ts +6 -0
- package/dist/commands/start.command.d.ts.map +1 -0
- package/dist/commands/start.command.js +64 -0
- package/dist/commands/start.command.js.map +1 -0
- package/dist/config/loadNurConfig.d.ts +3 -0
- package/dist/config/loadNurConfig.d.ts.map +1 -0
- package/dist/config/loadNurConfig.js +38 -0
- package/dist/config/loadNurConfig.js.map +1 -0
- package/dist/config/projectDetector.d.ts +3 -0
- package/dist/config/projectDetector.d.ts.map +1 -0
- package/dist/config/projectDetector.js +11 -0
- package/dist/config/projectDetector.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/utils/envFile.d.ts +2 -0
- package/dist/utils/envFile.d.ts.map +1 -0
- package/dist/utils/envFile.js +21 -0
- package/dist/utils/envFile.js.map +1 -0
- package/dist/utils/fileSystem.d.ts +11 -0
- package/dist/utils/fileSystem.d.ts.map +1 -0
- package/dist/utils/fileSystem.js +45 -0
- package/dist/utils/fileSystem.js.map +1 -0
- package/dist/utils/markers.d.ts +7 -0
- package/dist/utils/markers.d.ts.map +1 -0
- package/dist/utils/markers.js +22 -0
- package/dist/utils/markers.js.map +1 -0
- package/dist/utils/packageJson.d.ts +9 -0
- package/dist/utils/packageJson.d.ts.map +1 -0
- package/dist/utils/packageJson.js +21 -0
- package/dist/utils/packageJson.js.map +1 -0
- package/dist/utils/packageMetadata.d.ts +2 -0
- package/dist/utils/packageMetadata.d.ts.map +1 -0
- package/dist/utils/packageMetadata.js +8 -0
- package/dist/utils/packageMetadata.js.map +1 -0
- package/dist/utils/paths.d.ts +3 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +10 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/runCommand.d.ts +2 -0
- package/dist/utils/runCommand.d.ts.map +1 -0
- package/dist/utils/runCommand.js +19 -0
- package/dist/utils/runCommand.js.map +1 -0
- package/package.json +49 -0
- package/templates/app/.env.example.tpl +11 -0
- package/templates/app/.env.tpl +11 -0
- package/templates/app/.gitignore.tpl +5 -0
- package/templates/app/README.md.tpl +43 -0
- package/templates/app/nur.config.ts.tpl +29 -0
- package/templates/app/package.json.tpl +33 -0
- package/templates/app/pnpm-workspace.yaml.tpl +2 -0
- package/templates/app/src/app.ts.tpl +23 -0
- package/templates/app/src/config/app.config.ts.tpl +17 -0
- package/templates/app/src/config/env.ts.tpl +22 -0
- package/templates/app/src/main.ts.tpl +6 -0
- package/templates/app/src/middlewares/errorHandler.middleware.ts.tpl +23 -0
- package/templates/app/src/middlewares/notFound.middleware.ts.tpl +6 -0
- package/templates/app/src/middlewares/requestLogger.middleware.ts.tpl +17 -0
- package/templates/app/src/modules/health/health.controller.ts.tpl +12 -0
- package/templates/app/src/modules/health/health.module.ts.tpl +12 -0
- package/templates/app/src/modules/health/health.routes.ts.tpl +10 -0
- package/templates/app/src/modules/index.ts.tpl +12 -0
- package/templates/app/src/shared/errors/.gitkeep.tpl +0 -0
- package/templates/app/src/shared/logger/logger.ts.tpl +43 -0
- package/templates/app/src/shared/response/.gitkeep.tpl +0 -0
- package/templates/app/src/shared/validation/.gitkeep.tpl +0 -0
- package/templates/app/tsconfig.json.tpl +24 -0
- package/templates/modules/auth/files/src/modules/auth/auth.controller.ts.tpl +60 -0
- package/templates/modules/auth/files/src/modules/auth/auth.module.ts.tpl +22 -0
- package/templates/modules/auth/files/src/modules/auth/auth.repository.ts.tpl +22 -0
- package/templates/modules/auth/files/src/modules/auth/auth.routes.ts.tpl +21 -0
- package/templates/modules/auth/files/src/modules/auth/auth.schema.ts.tpl +19 -0
- package/templates/modules/auth/files/src/modules/auth/auth.service.ts.tpl +92 -0
- package/templates/modules/auth/files/src/modules/auth/auth.types.ts.tpl +14 -0
- package/templates/modules/auth/files/src/modules/auth/index.ts.tpl +1 -0
- package/templates/modules/auth/files/src/shared/security/jwt.ts.tpl +49 -0
- package/templates/modules/auth/files/src/shared/security/password.ts.tpl +11 -0
- package/templates/modules/auth/manifest.json +29 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { AppError, apiErrorResponse } from "nurjs-core";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { logger } from "../shared/logger/logger.js";
|
|
5
|
+
|
|
6
|
+
export function errorHandlerMiddleware(
|
|
7
|
+
error: unknown,
|
|
8
|
+
_req: Request,
|
|
9
|
+
res: Response,
|
|
10
|
+
_next: NextFunction,
|
|
11
|
+
): Response {
|
|
12
|
+
if (error instanceof z.ZodError) {
|
|
13
|
+
return res.status(400).json(apiErrorResponse("Validation failed", "VALIDATION_FAILED", z.flattenError(error)));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (error instanceof AppError) {
|
|
17
|
+
return res.status(error.statusCode).json(apiErrorResponse(error.message, error.code, error.details));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
logger.error("Unhandled error", error);
|
|
21
|
+
|
|
22
|
+
return res.status(500).json(apiErrorResponse("Internal server error", "INTERNAL_SERVER_ERROR"));
|
|
23
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { notFound } from "nurjs-core";
|
|
3
|
+
|
|
4
|
+
export function notFoundMiddleware(req: Request, _res: Response, next: NextFunction): void {
|
|
5
|
+
next(notFound(`Route not found: ${req.method} ${req.originalUrl}`));
|
|
6
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { logger } from "../shared/logger/logger.js";
|
|
3
|
+
|
|
4
|
+
export function requestLoggerMiddleware(req: Request, res: Response, next: NextFunction): void {
|
|
5
|
+
const startedAt = Date.now();
|
|
6
|
+
|
|
7
|
+
res.on("finish", () => {
|
|
8
|
+
logger.info("HTTP request", {
|
|
9
|
+
method: req.method,
|
|
10
|
+
path: req.originalUrl,
|
|
11
|
+
statusCode: res.statusCode,
|
|
12
|
+
durationMs: Date.now() - startedAt,
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
next();
|
|
17
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Request, Response } from "express";
|
|
2
|
+
import { apiResponse } from "nurjs-core";
|
|
3
|
+
|
|
4
|
+
export function getHealth(_req: Request, res: Response): Response {
|
|
5
|
+
return res.status(200).json(
|
|
6
|
+
apiResponse({
|
|
7
|
+
status: "ok",
|
|
8
|
+
engine: "NurJS",
|
|
9
|
+
timestamp: new Date().toISOString(),
|
|
10
|
+
}),
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NurModule } from "nurjs-core";
|
|
2
|
+
import { createHealthRouter } from "./health.routes.js";
|
|
3
|
+
|
|
4
|
+
export const healthModule: NurModule = {
|
|
5
|
+
name: "health",
|
|
6
|
+
basePath: "/health",
|
|
7
|
+
register: () => {
|
|
8
|
+
return {
|
|
9
|
+
router: createHealthRouter(),
|
|
10
|
+
};
|
|
11
|
+
},
|
|
12
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { NurModule } from "nurjs-core";
|
|
2
|
+
import { healthModule } from "./health/health.module.js";
|
|
3
|
+
|
|
4
|
+
// nur:imports:start
|
|
5
|
+
// nur:imports:end
|
|
6
|
+
|
|
7
|
+
export const modules: NurModule[] = [
|
|
8
|
+
healthModule,
|
|
9
|
+
|
|
10
|
+
// nur:modules:start
|
|
11
|
+
// nur:modules:end
|
|
12
|
+
];
|
|
File without changes
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import pino from "pino";
|
|
2
|
+
|
|
3
|
+
const pinoLogger = pino({
|
|
4
|
+
level: process.env.LOG_LEVEL ?? "info",
|
|
5
|
+
});
|
|
6
|
+
|
|
7
|
+
function normalizeMeta(meta?: unknown): Record<string, unknown> {
|
|
8
|
+
if (meta === undefined) {
|
|
9
|
+
return {};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (meta instanceof Error) {
|
|
13
|
+
return {
|
|
14
|
+
err: meta,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (typeof meta === "object" && meta !== null) {
|
|
19
|
+
return meta as Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
value: meta,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const logger = {
|
|
28
|
+
info(message: string, meta?: unknown): void {
|
|
29
|
+
pinoLogger.info(normalizeMeta(meta), message);
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
warn(message: string, meta?: unknown): void {
|
|
33
|
+
pinoLogger.warn(normalizeMeta(meta), message);
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
error(message: string, meta?: unknown): void {
|
|
37
|
+
pinoLogger.error(normalizeMeta(meta), message);
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
debug(message: string, meta?: unknown): void {
|
|
41
|
+
pinoLogger.debug(normalizeMeta(meta), message);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2024",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": [
|
|
7
|
+
"ES2024"
|
|
8
|
+
],
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"outDir": "dist",
|
|
14
|
+
"rootDir": "src",
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"types": [
|
|
18
|
+
"node"
|
|
19
|
+
]
|
|
20
|
+
},
|
|
21
|
+
"include": [
|
|
22
|
+
"src/**/*.ts"
|
|
23
|
+
]
|
|
24
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { NextFunction, Request, Response } from "express";
|
|
2
|
+
import { apiResponse, unauthorized } from "nurjs-core";
|
|
3
|
+
import type { AuthService } from "./auth.service.js";
|
|
4
|
+
|
|
5
|
+
type CreateAuthControllerOptions = {
|
|
6
|
+
authService: AuthService;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
function getBearerToken(req: Request): string | null {
|
|
10
|
+
const header = req.headers.authorization;
|
|
11
|
+
|
|
12
|
+
if (!header?.startsWith("Bearer ")) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return header.slice("Bearer ".length);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function createAuthController(options: CreateAuthControllerOptions) {
|
|
20
|
+
const { authService } = options;
|
|
21
|
+
|
|
22
|
+
return {
|
|
23
|
+
async register(req: Request, res: Response, next: NextFunction) {
|
|
24
|
+
try {
|
|
25
|
+
const user = await authService.register(req.body);
|
|
26
|
+
return res.status(201).json(apiResponse(user));
|
|
27
|
+
} catch (error) {
|
|
28
|
+
return next(error);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async login(req: Request, res: Response, next: NextFunction) {
|
|
33
|
+
try {
|
|
34
|
+
const result = await authService.login(req.body);
|
|
35
|
+
return res.status(200).json(apiResponse(result));
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return next(error);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
async logout(_req: Request, res: Response) {
|
|
42
|
+
return res.status(200).json(apiResponse({ loggedOut: true }));
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
async me(req: Request, res: Response, next: NextFunction) {
|
|
46
|
+
try {
|
|
47
|
+
const token = getBearerToken(req);
|
|
48
|
+
|
|
49
|
+
if (!token) {
|
|
50
|
+
throw unauthorized("Missing bearer token");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const user = await authService.getCurrentUserFromToken(token);
|
|
54
|
+
return res.status(200).json(apiResponse(user));
|
|
55
|
+
} catch (error) {
|
|
56
|
+
return next(error);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { NurModule } from "nurjs-core";
|
|
2
|
+
import { createAuthRepository } from "./auth.repository.js";
|
|
3
|
+
import { createAuthRouter } from "./auth.routes.js";
|
|
4
|
+
import { createAuthService } from "./auth.service.js";
|
|
5
|
+
|
|
6
|
+
export const authModule: NurModule = {
|
|
7
|
+
name: "auth",
|
|
8
|
+
basePath: "/auth",
|
|
9
|
+
register: (ctx) => {
|
|
10
|
+
const authRepository = createAuthRepository();
|
|
11
|
+
const authService = createAuthService({
|
|
12
|
+
authRepository,
|
|
13
|
+
logger: ctx.logger,
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
return {
|
|
17
|
+
router: createAuthRouter({
|
|
18
|
+
authService,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { AuthUser } from "./auth.types.js";
|
|
2
|
+
|
|
3
|
+
export type AuthRepository = ReturnType<typeof createAuthRepository>;
|
|
4
|
+
|
|
5
|
+
const users = new Map<string, AuthUser>();
|
|
6
|
+
|
|
7
|
+
export function createAuthRepository() {
|
|
8
|
+
return {
|
|
9
|
+
async create(user: AuthUser): Promise<AuthUser> {
|
|
10
|
+
users.set(user.id, user);
|
|
11
|
+
return user;
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
async findByEmail(email: string): Promise<AuthUser | null> {
|
|
15
|
+
return Array.from(users.values()).find((user) => user.email === email) ?? null;
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
async findById(id: string): Promise<AuthUser | null> {
|
|
19
|
+
return users.get(id) ?? null;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { validateRequest } from "nurjs-core";
|
|
3
|
+
import { createAuthController } from "./auth.controller.js";
|
|
4
|
+
import { loginSchema, registerSchema } from "./auth.schema.js";
|
|
5
|
+
import type { AuthService } from "./auth.service.js";
|
|
6
|
+
|
|
7
|
+
type CreateAuthRouterOptions = {
|
|
8
|
+
authService: AuthService;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createAuthRouter(options: CreateAuthRouterOptions): Router {
|
|
12
|
+
const router = Router();
|
|
13
|
+
const controller = createAuthController(options);
|
|
14
|
+
|
|
15
|
+
router.post("/register", validateRequest(registerSchema), controller.register);
|
|
16
|
+
router.post("/login", validateRequest(loginSchema), controller.login);
|
|
17
|
+
router.post("/logout", controller.logout);
|
|
18
|
+
router.get("/me", controller.me);
|
|
19
|
+
|
|
20
|
+
return router;
|
|
21
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const registerSchema = z.object({
|
|
4
|
+
body: z.object({
|
|
5
|
+
name: z.string().min(2),
|
|
6
|
+
email: z.email(),
|
|
7
|
+
password: z.string().min(8),
|
|
8
|
+
}),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export const loginSchema = z.object({
|
|
12
|
+
body: z.object({
|
|
13
|
+
email: z.email(),
|
|
14
|
+
password: z.string().min(1),
|
|
15
|
+
}),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export type RegisterInput = z.infer<typeof registerSchema>["body"];
|
|
19
|
+
export type LoginInput = z.infer<typeof loginSchema>["body"];
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { NurLogger } from "nurjs-core";
|
|
3
|
+
import { badRequest, unauthorized } from "nurjs-core";
|
|
4
|
+
import { comparePassword, hashPassword } from "../../shared/security/password.js";
|
|
5
|
+
import { signAuthToken, verifyAuthToken } from "../../shared/security/jwt.js";
|
|
6
|
+
import type { AuthRepository } from "./auth.repository.js";
|
|
7
|
+
import type { LoginInput, RegisterInput } from "./auth.schema.js";
|
|
8
|
+
import type { AuthTokenPayload, PublicAuthUser } from "./auth.types.js";
|
|
9
|
+
|
|
10
|
+
type CreateAuthServiceOptions = {
|
|
11
|
+
authRepository: AuthRepository;
|
|
12
|
+
logger: NurLogger;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export type AuthService = ReturnType<typeof createAuthService>;
|
|
16
|
+
|
|
17
|
+
function toPublicUser(user: { id: string; name: string; email: string; createdAt: string }): PublicAuthUser {
|
|
18
|
+
return {
|
|
19
|
+
id: user.id,
|
|
20
|
+
name: user.name,
|
|
21
|
+
email: user.email,
|
|
22
|
+
createdAt: user.createdAt,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function createAuthService(options: CreateAuthServiceOptions) {
|
|
27
|
+
const { authRepository, logger } = options;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
async register(input: RegisterInput) {
|
|
31
|
+
const existingUser = await authRepository.findByEmail(input.email);
|
|
32
|
+
|
|
33
|
+
if (existingUser) {
|
|
34
|
+
throw badRequest("Email is already registered");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const user = await authRepository.create({
|
|
38
|
+
id: randomUUID(),
|
|
39
|
+
name: input.name,
|
|
40
|
+
email: input.email,
|
|
41
|
+
passwordHash: await hashPassword(input.password),
|
|
42
|
+
createdAt: new Date().toISOString(),
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
logger.info("Auth user registered", { userId: user.id });
|
|
46
|
+
|
|
47
|
+
return toPublicUser(user);
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async login(input: LoginInput) {
|
|
51
|
+
const user = await authRepository.findByEmail(input.email);
|
|
52
|
+
|
|
53
|
+
if (!user) {
|
|
54
|
+
throw unauthorized("Invalid email or password");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const passwordMatches = await comparePassword(input.password, user.passwordHash);
|
|
58
|
+
|
|
59
|
+
if (!passwordMatches) {
|
|
60
|
+
throw unauthorized("Invalid email or password");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const token = signAuthToken({
|
|
64
|
+
sub: user.id,
|
|
65
|
+
email: user.email,
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
user: toPublicUser(user),
|
|
70
|
+
token,
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
async getCurrentUserFromToken(token: string) {
|
|
75
|
+
let payload: AuthTokenPayload;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
payload = verifyAuthToken<AuthTokenPayload>(token);
|
|
79
|
+
} catch {
|
|
80
|
+
throw unauthorized("Invalid or expired bearer token");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const user = await authRepository.findById(payload.sub);
|
|
84
|
+
|
|
85
|
+
if (!user) {
|
|
86
|
+
throw unauthorized("Authenticated user no longer exists");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return toPublicUser(user);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type AuthUser = {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
email: string;
|
|
5
|
+
passwordHash: string;
|
|
6
|
+
createdAt: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PublicAuthUser = Omit<AuthUser, "passwordHash">;
|
|
10
|
+
|
|
11
|
+
export type AuthTokenPayload = {
|
|
12
|
+
sub: string;
|
|
13
|
+
email: string;
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { authModule } from "./auth.module.js";
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import jwt, {
|
|
2
|
+
type JwtPayload,
|
|
3
|
+
type Secret,
|
|
4
|
+
type SignOptions,
|
|
5
|
+
} from "jsonwebtoken";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_DEV_JWT_SECRET = "nurjs-dev-secret-change-me";
|
|
8
|
+
|
|
9
|
+
function getJwtSecret(): Secret {
|
|
10
|
+
const secret = process.env.JWT_SECRET;
|
|
11
|
+
|
|
12
|
+
if (!secret && process.env.NODE_ENV === "production") {
|
|
13
|
+
throw new Error("JWT_SECRET is required in production.");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return secret ?? DEFAULT_DEV_JWT_SECRET;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function getJwtExpiresIn(): SignOptions["expiresIn"] {
|
|
20
|
+
return (process.env.JWT_EXPIRES_IN ?? "1d") as SignOptions["expiresIn"];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function signJwt(payload: string | object | Buffer): string {
|
|
24
|
+
return jwt.sign(payload, getJwtSecret(), {
|
|
25
|
+
expiresIn: getJwtExpiresIn(),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function verifyJwt<TPayload extends object = JwtPayload>(
|
|
30
|
+
token: string,
|
|
31
|
+
): TPayload {
|
|
32
|
+
const decoded = jwt.verify(token, getJwtSecret());
|
|
33
|
+
|
|
34
|
+
if (typeof decoded !== "object" || decoded === null) {
|
|
35
|
+
throw new Error("Invalid JWT payload.");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return decoded as TPayload;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function signAuthToken(payload: string | object | Buffer): string {
|
|
42
|
+
return signJwt(payload);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function verifyAuthToken<TPayload extends object = JwtPayload>(
|
|
46
|
+
token: string,
|
|
47
|
+
): TPayload {
|
|
48
|
+
return verifyJwt<TPayload>(token);
|
|
49
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import bcrypt from "bcryptjs";
|
|
2
|
+
|
|
3
|
+
const SALT_ROUNDS = 12;
|
|
4
|
+
|
|
5
|
+
export async function hashPassword(password: string): Promise<string> {
|
|
6
|
+
return bcrypt.hash(password, SALT_ROUNDS);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function comparePassword(password: string, passwordHash: string): Promise<boolean> {
|
|
10
|
+
return bcrypt.compare(password, passwordHash);
|
|
11
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "auth",
|
|
3
|
+
"description": "Authentication module skeleton",
|
|
4
|
+
"moduleImport": "import { authModule } from \"./auth/auth.module.js\";",
|
|
5
|
+
"moduleExportName": "authModule",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"bcryptjs": "^3.0.3",
|
|
8
|
+
"jsonwebtoken": "^9.0.3"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/jsonwebtoken": "^9.0.10"
|
|
12
|
+
},
|
|
13
|
+
"env": [
|
|
14
|
+
"JWT_SECRET=",
|
|
15
|
+
"JWT_EXPIRES_IN=1d"
|
|
16
|
+
],
|
|
17
|
+
"creates": [
|
|
18
|
+
"src/modules/auth/auth.module.ts",
|
|
19
|
+
"src/modules/auth/auth.routes.ts",
|
|
20
|
+
"src/modules/auth/auth.controller.ts",
|
|
21
|
+
"src/modules/auth/auth.service.ts",
|
|
22
|
+
"src/modules/auth/auth.repository.ts",
|
|
23
|
+
"src/modules/auth/auth.schema.ts",
|
|
24
|
+
"src/modules/auth/auth.types.ts",
|
|
25
|
+
"src/modules/auth/index.ts",
|
|
26
|
+
"src/shared/security/password.ts",
|
|
27
|
+
"src/shared/security/jwt.ts"
|
|
28
|
+
]
|
|
29
|
+
}
|