create-forgeon 0.1.34 → 0.1.36

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.
Files changed (38) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/src/modules/db-prisma.mjs +401 -0
  4. package/src/modules/executor.mjs +4 -0
  5. package/src/modules/executor.test.mjs +585 -13
  6. package/src/modules/i18n.mjs +244 -22
  7. package/src/modules/jwt-auth.mjs +612 -0
  8. package/src/modules/logger.mjs +76 -27
  9. package/src/modules/registry.mjs +15 -7
  10. package/src/modules/swagger.mjs +12 -2
  11. package/templates/module-fragments/db-prisma/00_title.md +6 -0
  12. package/templates/module-fragments/db-prisma/10_overview.md +10 -0
  13. package/templates/module-fragments/db-prisma/20_scope.md +14 -0
  14. package/templates/module-fragments/db-prisma/90_status_implemented.md +4 -0
  15. package/templates/module-fragments/jwt-auth/20_scope.md +17 -7
  16. package/templates/module-fragments/jwt-auth/90_status_implemented.md +7 -0
  17. package/templates/module-presets/jwt-auth/apps/api/prisma/migrations/0002_auth_refresh_token_hash/migration.sql +3 -0
  18. package/templates/module-presets/jwt-auth/apps/api/src/auth/prisma-auth-refresh-token.store.ts +36 -0
  19. package/templates/module-presets/jwt-auth/packages/auth-api/package.json +28 -0
  20. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.loader.ts +27 -0
  21. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.module.ts +8 -0
  22. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-config.service.ts +36 -0
  23. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-env.schema.ts +19 -0
  24. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth-refresh-token.store.ts +23 -0
  25. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.controller.ts +71 -0
  26. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.service.ts +155 -0
  27. package/templates/module-presets/jwt-auth/packages/auth-api/src/auth.types.ts +6 -0
  28. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/index.ts +2 -0
  29. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/login.dto.ts +11 -0
  30. package/templates/module-presets/jwt-auth/packages/auth-api/src/dto/refresh.dto.ts +8 -0
  31. package/templates/module-presets/jwt-auth/packages/auth-api/src/forgeon-auth.module.ts +47 -0
  32. package/templates/module-presets/jwt-auth/packages/auth-api/src/index.ts +12 -0
  33. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt-auth.guard.ts +5 -0
  34. package/templates/module-presets/jwt-auth/packages/auth-api/src/jwt.strategy.ts +20 -0
  35. package/templates/module-presets/jwt-auth/packages/auth-api/tsconfig.json +9 -0
  36. package/templates/module-presets/jwt-auth/packages/auth-contracts/package.json +21 -0
  37. package/templates/module-presets/jwt-auth/packages/auth-contracts/src/index.ts +47 -0
  38. package/templates/module-presets/jwt-auth/packages/auth-contracts/tsconfig.json +9 -0
@@ -0,0 +1,155 @@
1
+ import {
2
+ AUTH_ERROR_CODES,
3
+ AuthUser,
4
+ LoginResponse,
5
+ RefreshResponse,
6
+ TokenPair,
7
+ } from '@forgeon/auth-contracts';
8
+ import { Inject, Injectable, UnauthorizedException } from '@nestjs/common';
9
+ import { JwtService } from '@nestjs/jwt';
10
+ import { compare, hash } from 'bcryptjs';
11
+ import {
12
+ AUTH_REFRESH_TOKEN_STORE,
13
+ AuthRefreshTokenStore,
14
+ } from './auth-refresh-token.store';
15
+ import { AuthConfigService } from './auth-config.service';
16
+ import { LoginDto, RefreshDto } from './dto';
17
+ import { AuthJwtPayload } from './auth.types';
18
+
19
+ @Injectable()
20
+ export class AuthService {
21
+ constructor(
22
+ private readonly jwtService: JwtService,
23
+ private readonly configService: AuthConfigService,
24
+ @Inject(AUTH_REFRESH_TOKEN_STORE)
25
+ private readonly refreshTokenStore: AuthRefreshTokenStore,
26
+ ) {}
27
+
28
+ getTokenStoreKind(): string {
29
+ return this.refreshTokenStore.kind;
30
+ }
31
+
32
+ async login(input: LoginDto): Promise<LoginResponse> {
33
+ const email = input.email.trim().toLowerCase();
34
+ if (email !== this.configService.demoEmail || input.password !== this.configService.demoPassword) {
35
+ throw new UnauthorizedException({
36
+ message: 'Invalid credentials',
37
+ details: { code: AUTH_ERROR_CODES.invalidCredentials },
38
+ });
39
+ }
40
+
41
+ const user: AuthUser = {
42
+ sub: email,
43
+ email,
44
+ roles: ['user'],
45
+ };
46
+
47
+ const tokens = await this.issueTokens(user);
48
+ return {
49
+ ...tokens,
50
+ user,
51
+ tokenStore: this.refreshTokenStore.kind,
52
+ };
53
+ }
54
+
55
+ async refresh(input: RefreshDto): Promise<RefreshResponse> {
56
+ let payload: AuthJwtPayload;
57
+ try {
58
+ payload = await this.jwtService.verifyAsync<AuthJwtPayload>(input.refreshToken, {
59
+ secret: this.configService.refreshSecret,
60
+ });
61
+ } catch (error) {
62
+ const code =
63
+ error instanceof Error && error.name === 'TokenExpiredError'
64
+ ? AUTH_ERROR_CODES.tokenExpired
65
+ : AUTH_ERROR_CODES.refreshInvalid;
66
+
67
+ throw new UnauthorizedException({
68
+ message: 'Refresh token is invalid or expired',
69
+ details: { code },
70
+ });
71
+ }
72
+
73
+ if (this.refreshTokenStore.kind !== 'none') {
74
+ const storedHash = await this.refreshTokenStore.getRefreshTokenHash(payload.sub);
75
+ if (!storedHash) {
76
+ throw new UnauthorizedException({
77
+ message: 'Refresh token is invalid or expired',
78
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
79
+ });
80
+ }
81
+
82
+ const matched = await compare(input.refreshToken, storedHash);
83
+ if (!matched) {
84
+ throw new UnauthorizedException({
85
+ message: 'Refresh token is invalid or expired',
86
+ details: { code: AUTH_ERROR_CODES.refreshInvalid },
87
+ });
88
+ }
89
+ }
90
+
91
+ const user = this.toAuthUser(payload);
92
+ const tokens = await this.issueTokens(user);
93
+ return {
94
+ ...tokens,
95
+ user,
96
+ tokenStore: this.refreshTokenStore.kind,
97
+ };
98
+ }
99
+
100
+ async logout(user: AuthJwtPayload): Promise<void> {
101
+ if (this.refreshTokenStore.kind === 'none') {
102
+ return;
103
+ }
104
+ await this.refreshTokenStore.removeRefreshTokenHash(user.sub);
105
+ }
106
+
107
+ getProbeStatus() {
108
+ return {
109
+ status: 'ok',
110
+ feature: 'jwt-auth',
111
+ tokenStore: this.refreshTokenStore.kind,
112
+ demoEmail: this.configService.demoEmail,
113
+ };
114
+ }
115
+
116
+ private async issueTokens(user: AuthUser): Promise<TokenPair> {
117
+ const payload: AuthJwtPayload = {
118
+ sub: user.sub,
119
+ email: user.email,
120
+ roles: user.roles,
121
+ };
122
+
123
+ const [accessToken, refreshToken] = await Promise.all([
124
+ this.jwtService.signAsync(payload, {
125
+ secret: this.configService.accessSecret,
126
+ expiresIn: this.configService.accessExpiresIn,
127
+ }),
128
+ this.jwtService.signAsync(payload, {
129
+ secret: this.configService.refreshSecret,
130
+ expiresIn: this.configService.refreshExpiresIn,
131
+ }),
132
+ ]);
133
+
134
+ if (this.refreshTokenStore.kind !== 'none') {
135
+ const tokenHash = await hash(refreshToken, this.configService.bcryptRounds);
136
+ await this.refreshTokenStore.saveRefreshTokenHash(user.sub, tokenHash);
137
+ }
138
+
139
+ return {
140
+ accessToken,
141
+ refreshToken,
142
+ tokenType: 'Bearer',
143
+ accessTtl: this.configService.accessExpiresIn,
144
+ refreshTtl: this.configService.refreshExpiresIn,
145
+ };
146
+ }
147
+
148
+ private toAuthUser(payload: AuthJwtPayload): AuthUser {
149
+ return {
150
+ sub: payload.sub,
151
+ email: payload.email,
152
+ roles: Array.isArray(payload.roles) ? payload.roles : ['user'],
153
+ };
154
+ }
155
+ }
@@ -0,0 +1,6 @@
1
+ import type { AuthUser } from '@forgeon/auth-contracts';
2
+
3
+ export interface AuthJwtPayload extends AuthUser {
4
+ iat?: number;
5
+ exp?: number;
6
+ }
@@ -0,0 +1,2 @@
1
+ export * from './login.dto';
2
+ export * from './refresh.dto';
@@ -0,0 +1,11 @@
1
+ import { LoginRequest } from '@forgeon/auth-contracts';
2
+ import { IsEmail, IsString, MinLength } from 'class-validator';
3
+
4
+ export class LoginDto implements LoginRequest {
5
+ @IsEmail()
6
+ email!: string;
7
+
8
+ @IsString()
9
+ @MinLength(8)
10
+ password!: string;
11
+ }
@@ -0,0 +1,8 @@
1
+ import { RefreshRequest } from '@forgeon/auth-contracts';
2
+ import { IsString, MinLength } from 'class-validator';
3
+
4
+ export class RefreshDto implements RefreshRequest {
5
+ @IsString()
6
+ @MinLength(16)
7
+ refreshToken!: string;
8
+ }
@@ -0,0 +1,47 @@
1
+ import {
2
+ DynamicModule,
3
+ Module,
4
+ ModuleMetadata,
5
+ Provider,
6
+ } from '@nestjs/common';
7
+ import { JwtModule } from '@nestjs/jwt';
8
+ import { PassportModule } from '@nestjs/passport';
9
+ import {
10
+ AUTH_REFRESH_TOKEN_STORE,
11
+ NoopAuthRefreshTokenStore,
12
+ } from './auth-refresh-token.store';
13
+ import { AuthConfigModule } from './auth-config.module';
14
+ import { AuthController } from './auth.controller';
15
+ import { AuthService } from './auth.service';
16
+ import { JwtAuthGuard } from './jwt-auth.guard';
17
+ import { JwtStrategy } from './jwt.strategy';
18
+
19
+ export interface ForgeonAuthModuleOptions {
20
+ imports?: ModuleMetadata['imports'];
21
+ refreshTokenStoreProvider?: Provider;
22
+ }
23
+
24
+ @Module({})
25
+ export class ForgeonAuthModule {
26
+ static register(options: ForgeonAuthModuleOptions = {}): DynamicModule {
27
+ const refreshTokenStoreProvider =
28
+ options.refreshTokenStoreProvider ??
29
+ ({
30
+ provide: AUTH_REFRESH_TOKEN_STORE,
31
+ useClass: NoopAuthRefreshTokenStore,
32
+ } satisfies Provider);
33
+
34
+ return {
35
+ module: ForgeonAuthModule,
36
+ imports: [
37
+ AuthConfigModule,
38
+ PassportModule.register({ defaultStrategy: 'jwt' }),
39
+ JwtModule.register({}),
40
+ ...(options.imports ?? []),
41
+ ],
42
+ controllers: [AuthController],
43
+ providers: [AuthService, JwtStrategy, JwtAuthGuard, refreshTokenStoreProvider],
44
+ exports: [AuthConfigModule, AuthService, JwtAuthGuard, AUTH_REFRESH_TOKEN_STORE],
45
+ };
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ export * from './auth-env.schema';
2
+ export * from './auth-config.loader';
3
+ export * from './auth-config.module';
4
+ export * from './auth-config.service';
5
+ export * from './auth-refresh-token.store';
6
+ export * from './auth.controller';
7
+ export * from './auth.service';
8
+ export * from './auth.types';
9
+ export * from './dto';
10
+ export * from './forgeon-auth.module';
11
+ export * from './jwt-auth.guard';
12
+ export * from './jwt.strategy';
@@ -0,0 +1,5 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { AuthGuard } from '@nestjs/passport';
3
+
4
+ @Injectable()
5
+ export class JwtAuthGuard extends AuthGuard('jwt') {}
@@ -0,0 +1,20 @@
1
+ import { Injectable } from '@nestjs/common';
2
+ import { PassportStrategy } from '@nestjs/passport';
3
+ import { ExtractJwt, Strategy } from 'passport-jwt';
4
+ import { AuthConfigService } from './auth-config.service';
5
+ import { AuthJwtPayload } from './auth.types';
6
+
7
+ @Injectable()
8
+ export class JwtStrategy extends PassportStrategy(Strategy) {
9
+ constructor(configService: AuthConfigService) {
10
+ super({
11
+ jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
12
+ ignoreExpiration: false,
13
+ secretOrKey: configService.accessSecret,
14
+ });
15
+ }
16
+
17
+ validate(payload: AuthJwtPayload): AuthJwtPayload {
18
+ return payload;
19
+ }
20
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.node.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@forgeon/auth-contracts",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.10.7",
19
+ "typescript": "^5.7.3"
20
+ }
21
+ }
@@ -0,0 +1,47 @@
1
+ export const AUTH_API_ROUTES = {
2
+ login: '/api/auth/login',
3
+ refresh: '/api/auth/refresh',
4
+ logout: '/api/auth/logout',
5
+ me: '/api/auth/me',
6
+ } as const;
7
+
8
+ export const AUTH_ERROR_CODES = {
9
+ invalidCredentials: 'AUTH_INVALID_CREDENTIALS',
10
+ tokenExpired: 'AUTH_TOKEN_EXPIRED',
11
+ refreshInvalid: 'AUTH_REFRESH_INVALID',
12
+ } as const;
13
+
14
+ export type AuthErrorCode = (typeof AUTH_ERROR_CODES)[keyof typeof AUTH_ERROR_CODES];
15
+
16
+ export interface AuthUser {
17
+ sub: string;
18
+ email: string;
19
+ roles: string[];
20
+ }
21
+
22
+ export interface LoginRequest {
23
+ email: string;
24
+ password: string;
25
+ }
26
+
27
+ export interface RefreshRequest {
28
+ refreshToken: string;
29
+ }
30
+
31
+ export interface TokenPair {
32
+ accessToken: string;
33
+ refreshToken: string;
34
+ tokenType: 'Bearer';
35
+ accessTtl: string;
36
+ refreshTtl: string;
37
+ }
38
+
39
+ export interface LoginResponse extends TokenPair {
40
+ user: AuthUser;
41
+ tokenStore: string;
42
+ }
43
+
44
+ export interface RefreshResponse extends TokenPair {
45
+ user: AuthUser;
46
+ tokenStore: string;
47
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.esm.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "dist",
6
+ "types": ["node"]
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }