@codecanva/nest-auth 0.1.0 → 0.2.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/INSTALLATION.md +682 -0
- package/README.md +200 -164
- package/dist/auth.module.d.ts.map +1 -1
- package/dist/auth.module.js +6 -0
- package/dist/auth.module.js.map +1 -1
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +270 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/templates.d.ts +21 -0
- package/dist/cli/templates.d.ts.map +1 -0
- package/dist/cli/templates.js +556 -0
- package/dist/cli/templates.js.map +1 -0
- package/dist/cli/utils.d.ts +55 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +235 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/filters/auth-exception.filter.d.ts +20 -0
- package/dist/filters/auth-exception.filter.d.ts.map +1 -0
- package/dist/filters/auth-exception.filter.js +40 -0
- package/dist/filters/auth-exception.filter.js.map +1 -0
- package/dist/filters/index.d.ts +2 -0
- package/dist/filters/index.d.ts.map +1 -0
- package/dist/filters/index.js +18 -0
- package/dist/filters/index.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/package.json +112 -107
|
@@ -0,0 +1,556 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// File templates emitted by `nest-auth init`.
|
|
4
|
+
//
|
|
5
|
+
// Every template imports the runtime pieces from the installed package (whose
|
|
6
|
+
// name is injected as `pkg`), so generated code always lines up with what the
|
|
7
|
+
// consumer actually has in node_modules.
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.buildPlan = buildPlan;
|
|
11
|
+
const ENV = [
|
|
12
|
+
{
|
|
13
|
+
key: 'JWT_ACCESS_SECRET',
|
|
14
|
+
value: 'change-me-access-secret-min-32-chars-long-please',
|
|
15
|
+
comment: 'nest-auth: access-token signing secret (use a strong random value)',
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
key: 'JWT_REFRESH_SECRET',
|
|
19
|
+
value: 'change-me-refresh-secret-min-32-chars-long-different',
|
|
20
|
+
comment: 'nest-auth: refresh-token signing secret (different from access)',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
key: 'TOKEN_HASH_PEPPER',
|
|
24
|
+
value: 'change-me-pepper',
|
|
25
|
+
comment: 'nest-auth: extra pepper mixed into stored refresh-token hashes',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
// --- Shared ----------------------------------------------------------------
|
|
29
|
+
function authController(pkg) {
|
|
30
|
+
return `import { Body, Controller, Get, HttpCode, Post } from '@nestjs/common';
|
|
31
|
+
import {
|
|
32
|
+
AuthService,
|
|
33
|
+
AuthUser,
|
|
34
|
+
CurrentUser,
|
|
35
|
+
LoginDto,
|
|
36
|
+
Public,
|
|
37
|
+
RefreshTokenDto,
|
|
38
|
+
} from '${pkg}';
|
|
39
|
+
|
|
40
|
+
// AuthError domain errors (e.g. InvalidCredentialsError) are mapped to the
|
|
41
|
+
// right HTTP status by the global AuthExceptionFilter that AuthModule
|
|
42
|
+
// registers — so these handlers can return the service calls directly.
|
|
43
|
+
@Controller('auth')
|
|
44
|
+
export class AuthController {
|
|
45
|
+
constructor(private readonly auth: AuthService) {}
|
|
46
|
+
|
|
47
|
+
@Public()
|
|
48
|
+
@Post('login')
|
|
49
|
+
@HttpCode(200)
|
|
50
|
+
login(@Body() dto: LoginDto) {
|
|
51
|
+
return this.auth.login(dto.email, dto.password);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@Public()
|
|
55
|
+
@Post('refresh')
|
|
56
|
+
@HttpCode(200)
|
|
57
|
+
refresh(@Body() dto: RefreshTokenDto) {
|
|
58
|
+
return this.auth.refresh(dto.refreshToken);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Post('logout')
|
|
62
|
+
@HttpCode(204)
|
|
63
|
+
logout(@Body() dto: RefreshTokenDto) {
|
|
64
|
+
return this.auth.logout(dto.refreshToken);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Get('me')
|
|
68
|
+
me(@CurrentUser() user: AuthUser) {
|
|
69
|
+
return user;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
`;
|
|
73
|
+
}
|
|
74
|
+
// --- Mongoose persistence --------------------------------------------------
|
|
75
|
+
function mongoosePlan(pkg) {
|
|
76
|
+
const files = [
|
|
77
|
+
{
|
|
78
|
+
rel: 'user/schemas/user.schema.ts',
|
|
79
|
+
content: `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
80
|
+
import { HydratedDocument } from 'mongoose';
|
|
81
|
+
|
|
82
|
+
export type UserDocument = HydratedDocument<User>;
|
|
83
|
+
|
|
84
|
+
@Schema({ timestamps: true })
|
|
85
|
+
export class User {
|
|
86
|
+
@Prop({ required: true, unique: true, lowercase: true, trim: true, index: true })
|
|
87
|
+
email: string;
|
|
88
|
+
|
|
89
|
+
@Prop({ required: true })
|
|
90
|
+
passwordHash: string;
|
|
91
|
+
|
|
92
|
+
@Prop({ type: [String], default: [] })
|
|
93
|
+
roles: string[];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const UserSchema = SchemaFactory.createForClass(User);
|
|
97
|
+
`,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
rel: 'user/dto/create-user.dto.ts',
|
|
101
|
+
content: `import { IsEmail, IsString, MinLength } from 'class-validator';
|
|
102
|
+
|
|
103
|
+
export class CreateUserDto {
|
|
104
|
+
@IsEmail()
|
|
105
|
+
email: string;
|
|
106
|
+
|
|
107
|
+
@IsString()
|
|
108
|
+
@MinLength(8)
|
|
109
|
+
password: string;
|
|
110
|
+
}
|
|
111
|
+
`,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
rel: 'user/user.service.ts',
|
|
115
|
+
content: `import { ConflictException, Injectable } from '@nestjs/common';
|
|
116
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
117
|
+
import { Model } from 'mongoose';
|
|
118
|
+
import * as bcrypt from 'bcrypt';
|
|
119
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
120
|
+
import { User, UserDocument } from './schemas/user.schema';
|
|
121
|
+
|
|
122
|
+
const BCRYPT_ROUNDS = 12;
|
|
123
|
+
|
|
124
|
+
@Injectable()
|
|
125
|
+
export class UserService {
|
|
126
|
+
constructor(
|
|
127
|
+
@InjectModel(User.name) private readonly userModel: Model<UserDocument>,
|
|
128
|
+
) {}
|
|
129
|
+
|
|
130
|
+
async create(dto: CreateUserDto): Promise<UserDocument> {
|
|
131
|
+
const existing = await this.userModel
|
|
132
|
+
.findOne({ email: dto.email.toLowerCase() })
|
|
133
|
+
.lean();
|
|
134
|
+
if (existing) throw new ConflictException('Email already registered');
|
|
135
|
+
const passwordHash = await bcrypt.hash(dto.password, BCRYPT_ROUNDS);
|
|
136
|
+
return this.userModel.create({ email: dto.email, passwordHash });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
findByEmail(email: string): Promise<UserDocument | null> {
|
|
140
|
+
return this.userModel.findOne({ email: email.toLowerCase() }).exec();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
findById(id: string): Promise<UserDocument | null> {
|
|
144
|
+
return this.userModel.findById(id).exec();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
`,
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
rel: 'user/user.validator.ts',
|
|
151
|
+
content: `import { Injectable } from '@nestjs/common';
|
|
152
|
+
import { AuthUser, UserValidator } from '${pkg}';
|
|
153
|
+
import * as bcrypt from 'bcrypt';
|
|
154
|
+
import { UserService } from './user.service';
|
|
155
|
+
import { UserDocument } from './schemas/user.schema';
|
|
156
|
+
|
|
157
|
+
@Injectable()
|
|
158
|
+
export class MyUserValidator implements UserValidator {
|
|
159
|
+
constructor(private readonly users: UserService) {}
|
|
160
|
+
|
|
161
|
+
async validateCredentials(
|
|
162
|
+
email: string,
|
|
163
|
+
password: string,
|
|
164
|
+
): Promise<AuthUser | null> {
|
|
165
|
+
const user = await this.users.findByEmail(email);
|
|
166
|
+
if (!user) return null;
|
|
167
|
+
const ok = await bcrypt.compare(password, user.passwordHash);
|
|
168
|
+
if (!ok) return null;
|
|
169
|
+
return this.toAuthUser(user);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async findById(userId: string | number): Promise<AuthUser | null> {
|
|
173
|
+
const user = await this.users.findById(String(userId));
|
|
174
|
+
return user ? this.toAuthUser(user) : null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private toAuthUser(user: UserDocument): AuthUser {
|
|
178
|
+
return { id: user._id.toString(), email: user.email, roles: user.roles };
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
`,
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
rel: 'user/user.controller.ts',
|
|
185
|
+
content: `import { Body, Controller, Post } from '@nestjs/common';
|
|
186
|
+
import { Public } from '${pkg}';
|
|
187
|
+
import { UserService } from './user.service';
|
|
188
|
+
import { CreateUserDto } from './dto/create-user.dto';
|
|
189
|
+
|
|
190
|
+
@Controller('user')
|
|
191
|
+
export class UserController {
|
|
192
|
+
constructor(private readonly userService: UserService) {}
|
|
193
|
+
|
|
194
|
+
@Public()
|
|
195
|
+
@Post('register')
|
|
196
|
+
async register(@Body() dto: CreateUserDto) {
|
|
197
|
+
const user = await this.userService.create(dto);
|
|
198
|
+
return { id: user._id.toString(), email: user.email, roles: user.roles };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
`,
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
rel: 'user/user.module.ts',
|
|
205
|
+
content: `import { Module } from '@nestjs/common';
|
|
206
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
207
|
+
import { UserService } from './user.service';
|
|
208
|
+
import { UserController } from './user.controller';
|
|
209
|
+
import { MyUserValidator } from './user.validator';
|
|
210
|
+
import { User, UserSchema } from './schemas/user.schema';
|
|
211
|
+
|
|
212
|
+
@Module({
|
|
213
|
+
imports: [
|
|
214
|
+
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
|
215
|
+
],
|
|
216
|
+
controllers: [UserController],
|
|
217
|
+
providers: [UserService, MyUserValidator],
|
|
218
|
+
exports: [UserService, MyUserValidator],
|
|
219
|
+
})
|
|
220
|
+
export class UserModule {}
|
|
221
|
+
`,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
rel: 'auth/schemas/refresh-token.schema.ts',
|
|
225
|
+
content: `import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose';
|
|
226
|
+
import { HydratedDocument } from 'mongoose';
|
|
227
|
+
|
|
228
|
+
export type RefreshTokenDocument = HydratedDocument<RefreshToken>;
|
|
229
|
+
|
|
230
|
+
@Schema({
|
|
231
|
+
collection: 'refresh_tokens',
|
|
232
|
+
timestamps: { createdAt: true, updatedAt: false },
|
|
233
|
+
})
|
|
234
|
+
export class RefreshToken {
|
|
235
|
+
@Prop({ type: String, required: true })
|
|
236
|
+
_id: string;
|
|
237
|
+
|
|
238
|
+
@Prop({ type: String, required: true, index: true })
|
|
239
|
+
userId: string;
|
|
240
|
+
|
|
241
|
+
@Prop({ type: String, required: true, index: true })
|
|
242
|
+
tokenHash: string;
|
|
243
|
+
|
|
244
|
+
@Prop({ type: Object })
|
|
245
|
+
metadata?: Record<string, unknown>;
|
|
246
|
+
|
|
247
|
+
@Prop({ type: Date, required: true, index: { expires: 0 } })
|
|
248
|
+
expiresAt: Date;
|
|
249
|
+
|
|
250
|
+
@Prop({ type: Date, default: null })
|
|
251
|
+
revokedAt: Date | null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
export const RefreshTokenSchema = SchemaFactory.createForClass(RefreshToken);
|
|
255
|
+
`,
|
|
256
|
+
},
|
|
257
|
+
{
|
|
258
|
+
rel: 'auth/refresh-token.store.ts',
|
|
259
|
+
content: `import { Injectable } from '@nestjs/common';
|
|
260
|
+
import { InjectModel } from '@nestjs/mongoose';
|
|
261
|
+
import { Model } from 'mongoose';
|
|
262
|
+
import {
|
|
263
|
+
CreateRefreshTokenInput,
|
|
264
|
+
RefreshTokenStore,
|
|
265
|
+
StoredRefreshToken,
|
|
266
|
+
} from '${pkg}';
|
|
267
|
+
import {
|
|
268
|
+
RefreshToken,
|
|
269
|
+
RefreshTokenDocument,
|
|
270
|
+
} from './schemas/refresh-token.schema';
|
|
271
|
+
|
|
272
|
+
@Injectable()
|
|
273
|
+
export class MyRefreshTokenStore implements RefreshTokenStore {
|
|
274
|
+
constructor(
|
|
275
|
+
@InjectModel(RefreshToken.name)
|
|
276
|
+
private readonly model: Model<RefreshTokenDocument>,
|
|
277
|
+
) {}
|
|
278
|
+
|
|
279
|
+
async create(input: CreateRefreshTokenInput): Promise<StoredRefreshToken> {
|
|
280
|
+
const doc = await this.model.create({
|
|
281
|
+
_id: input.id,
|
|
282
|
+
userId: String(input.userId),
|
|
283
|
+
tokenHash: input.tokenHash,
|
|
284
|
+
expiresAt: input.expiresAt,
|
|
285
|
+
metadata: input.metadata,
|
|
286
|
+
revokedAt: null,
|
|
287
|
+
});
|
|
288
|
+
return this.toStored(doc.toObject());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async findById(id: string): Promise<StoredRefreshToken | null> {
|
|
292
|
+
const doc = await this.model.findById(id).lean();
|
|
293
|
+
return doc ? this.toStored(doc) : null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Atomic: only revoke + return the row if it was unrevoked AND the hash matches.
|
|
297
|
+
async consume(
|
|
298
|
+
id: string,
|
|
299
|
+
expectedHash: string,
|
|
300
|
+
): Promise<StoredRefreshToken | null> {
|
|
301
|
+
const doc = await this.model
|
|
302
|
+
.findOneAndUpdate(
|
|
303
|
+
{ _id: id, tokenHash: expectedHash, revokedAt: null },
|
|
304
|
+
{ $set: { revokedAt: new Date() } },
|
|
305
|
+
{ new: false, lean: true },
|
|
306
|
+
)
|
|
307
|
+
.exec();
|
|
308
|
+
return doc ? this.toStored(doc) : null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async revokeById(id: string): Promise<void> {
|
|
312
|
+
await this.model
|
|
313
|
+
.updateOne({ _id: id, revokedAt: null }, { $set: { revokedAt: new Date() } })
|
|
314
|
+
.exec();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async revokeAllForUser(userId: string | number): Promise<void> {
|
|
318
|
+
await this.model
|
|
319
|
+
.updateMany(
|
|
320
|
+
{ userId: String(userId), revokedAt: null },
|
|
321
|
+
{ $set: { revokedAt: new Date() } },
|
|
322
|
+
)
|
|
323
|
+
.exec();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
private toStored(doc: any): StoredRefreshToken {
|
|
327
|
+
return {
|
|
328
|
+
id: String(doc._id),
|
|
329
|
+
userId: doc.userId,
|
|
330
|
+
tokenHash: doc.tokenHash,
|
|
331
|
+
metadata: doc.metadata,
|
|
332
|
+
expiresAt: doc.expiresAt,
|
|
333
|
+
revokedAt: doc.revokedAt ?? null,
|
|
334
|
+
createdAt: doc.createdAt,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
`,
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
rel: 'auth/auth-persistence.module.ts',
|
|
342
|
+
content: `import { Module } from '@nestjs/common';
|
|
343
|
+
import { MongooseModule } from '@nestjs/mongoose';
|
|
344
|
+
import { MyRefreshTokenStore } from './refresh-token.store';
|
|
345
|
+
import {
|
|
346
|
+
RefreshToken,
|
|
347
|
+
RefreshTokenSchema,
|
|
348
|
+
} from './schemas/refresh-token.schema';
|
|
349
|
+
|
|
350
|
+
@Module({
|
|
351
|
+
imports: [
|
|
352
|
+
MongooseModule.forFeature([
|
|
353
|
+
{ name: RefreshToken.name, schema: RefreshTokenSchema },
|
|
354
|
+
]),
|
|
355
|
+
],
|
|
356
|
+
providers: [MyRefreshTokenStore],
|
|
357
|
+
exports: [MyRefreshTokenStore],
|
|
358
|
+
})
|
|
359
|
+
export class AuthPersistenceModule {}
|
|
360
|
+
`,
|
|
361
|
+
},
|
|
362
|
+
{ rel: 'auth/auth.controller.ts', content: authController(pkg) },
|
|
363
|
+
{
|
|
364
|
+
rel: 'auth/auth-integration.module.ts',
|
|
365
|
+
content: `import { Module } from '@nestjs/common';
|
|
366
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
367
|
+
import { AuthModule, JwtAuthGuard } from '${pkg}';
|
|
368
|
+
import { UserModule } from '../user/user.module';
|
|
369
|
+
import { MyUserValidator } from '../user/user.validator';
|
|
370
|
+
import { AuthPersistenceModule } from './auth-persistence.module';
|
|
371
|
+
import { MyRefreshTokenStore } from './refresh-token.store';
|
|
372
|
+
import { AuthController } from './auth.controller';
|
|
373
|
+
|
|
374
|
+
// Single drop-in module: import this in your AppModule and you're wired.
|
|
375
|
+
// Requires a Mongoose connection (MongooseModule.forRoot) at the app root.
|
|
376
|
+
@Module({
|
|
377
|
+
imports: [
|
|
378
|
+
UserModule,
|
|
379
|
+
AuthPersistenceModule,
|
|
380
|
+
AuthModule.forRootAsync({
|
|
381
|
+
imports: [UserModule, AuthPersistenceModule],
|
|
382
|
+
useFactory: () => ({
|
|
383
|
+
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'dev-access-secret-change-me',
|
|
384
|
+
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'dev-refresh-secret-change-me',
|
|
385
|
+
accessTtl: '15m',
|
|
386
|
+
refreshTtl: '30d',
|
|
387
|
+
tokenHashPepper: process.env.TOKEN_HASH_PEPPER,
|
|
388
|
+
issuer: 'nest-auth',
|
|
389
|
+
}),
|
|
390
|
+
store: { useExisting: MyRefreshTokenStore },
|
|
391
|
+
validator: { useExisting: MyUserValidator },
|
|
392
|
+
}),
|
|
393
|
+
],
|
|
394
|
+
controllers: [AuthController],
|
|
395
|
+
// APP_GUARD provided here applies globally: every route requires a valid
|
|
396
|
+
// access token unless decorated with @Public().
|
|
397
|
+
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
|
|
398
|
+
})
|
|
399
|
+
export class AuthIntegrationModule {}
|
|
400
|
+
`,
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
return {
|
|
404
|
+
files,
|
|
405
|
+
env: ENV,
|
|
406
|
+
integrationModule: 'AuthIntegrationModule',
|
|
407
|
+
integrationImportPath: './auth/auth-integration.module',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
// --- In-memory (quick start / trial) ---------------------------------------
|
|
411
|
+
function memoryPlan(pkg) {
|
|
412
|
+
const files = [
|
|
413
|
+
{
|
|
414
|
+
rel: 'auth/in-memory-user.validator.ts',
|
|
415
|
+
content: `import { Injectable } from '@nestjs/common';
|
|
416
|
+
import { AuthUser, UserValidator } from '${pkg}';
|
|
417
|
+
|
|
418
|
+
// PLACEHOLDER for quick trials only — replace with your real user lookup
|
|
419
|
+
// (Mongoose/TypeORM/Prisma) and bcrypt password comparison before shipping.
|
|
420
|
+
interface DemoUser extends AuthUser {
|
|
421
|
+
password: string;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
@Injectable()
|
|
425
|
+
export class InMemoryUserValidator implements UserValidator {
|
|
426
|
+
private readonly users: DemoUser[] = [
|
|
427
|
+
{ id: 1, email: 'demo@example.com', password: 'password123', roles: ['user'] },
|
|
428
|
+
];
|
|
429
|
+
|
|
430
|
+
async validateCredentials(
|
|
431
|
+
email: string,
|
|
432
|
+
password: string,
|
|
433
|
+
): Promise<AuthUser | null> {
|
|
434
|
+
const user = this.users.find((u) => u.email === email);
|
|
435
|
+
if (!user || user.password !== password) return null;
|
|
436
|
+
const { password: _omit, ...safe } = user;
|
|
437
|
+
return safe;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async findById(userId: string | number): Promise<AuthUser | null> {
|
|
441
|
+
const user = this.users.find((u) => u.id === userId);
|
|
442
|
+
if (!user) return null;
|
|
443
|
+
const { password: _omit, ...safe } = user;
|
|
444
|
+
return safe;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
`,
|
|
448
|
+
},
|
|
449
|
+
{
|
|
450
|
+
rel: 'auth/in-memory-refresh-token.store.ts',
|
|
451
|
+
content: `import { Injectable } from '@nestjs/common';
|
|
452
|
+
import {
|
|
453
|
+
CreateRefreshTokenInput,
|
|
454
|
+
RefreshTokenStore,
|
|
455
|
+
StoredRefreshToken,
|
|
456
|
+
} from '${pkg}';
|
|
457
|
+
|
|
458
|
+
// PLACEHOLDER for quick trials only — sessions live in memory and are lost on
|
|
459
|
+
// restart. Swap for a persistent store before shipping.
|
|
460
|
+
@Injectable()
|
|
461
|
+
export class InMemoryRefreshTokenStore implements RefreshTokenStore {
|
|
462
|
+
private readonly rows = new Map<string, StoredRefreshToken>();
|
|
463
|
+
|
|
464
|
+
async create(input: CreateRefreshTokenInput): Promise<StoredRefreshToken> {
|
|
465
|
+
const row: StoredRefreshToken = {
|
|
466
|
+
id: input.id,
|
|
467
|
+
userId: input.userId,
|
|
468
|
+
tokenHash: input.tokenHash,
|
|
469
|
+
expiresAt: input.expiresAt,
|
|
470
|
+
metadata: input.metadata,
|
|
471
|
+
revokedAt: null,
|
|
472
|
+
createdAt: new Date(),
|
|
473
|
+
};
|
|
474
|
+
this.rows.set(row.id, row);
|
|
475
|
+
return row;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async findById(id: string): Promise<StoredRefreshToken | null> {
|
|
479
|
+
return this.rows.get(id) ?? null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
async consume(
|
|
483
|
+
id: string,
|
|
484
|
+
expectedHash: string,
|
|
485
|
+
): Promise<StoredRefreshToken | null> {
|
|
486
|
+
const row = this.rows.get(id);
|
|
487
|
+
if (!row || row.revokedAt) return null;
|
|
488
|
+
if (row.tokenHash !== expectedHash) return null;
|
|
489
|
+
const consumed: StoredRefreshToken = { ...row, revokedAt: new Date() };
|
|
490
|
+
this.rows.set(id, consumed);
|
|
491
|
+
return consumed;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
async revokeById(id: string): Promise<void> {
|
|
495
|
+
const row = this.rows.get(id);
|
|
496
|
+
if (!row || row.revokedAt) return;
|
|
497
|
+
this.rows.set(id, { ...row, revokedAt: new Date() });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async revokeAllForUser(userId: string | number): Promise<void> {
|
|
501
|
+
const now = new Date();
|
|
502
|
+
for (const [id, row] of this.rows) {
|
|
503
|
+
if (row.userId === userId && !row.revokedAt) {
|
|
504
|
+
this.rows.set(id, { ...row, revokedAt: now });
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
`,
|
|
510
|
+
},
|
|
511
|
+
{ rel: 'auth/auth.controller.ts', content: authController(pkg) },
|
|
512
|
+
{
|
|
513
|
+
rel: 'auth/auth-integration.module.ts',
|
|
514
|
+
content: `import { Module } from '@nestjs/common';
|
|
515
|
+
import { APP_GUARD } from '@nestjs/core';
|
|
516
|
+
import { AuthModule, JwtAuthGuard } from '${pkg}';
|
|
517
|
+
import { InMemoryUserValidator } from './in-memory-user.validator';
|
|
518
|
+
import { InMemoryRefreshTokenStore } from './in-memory-refresh-token.store';
|
|
519
|
+
import { AuthController } from './auth.controller';
|
|
520
|
+
|
|
521
|
+
// Single drop-in module: import this in your AppModule and you're wired.
|
|
522
|
+
// Uses in-memory store + validator — great for trying things out, NOT for prod.
|
|
523
|
+
@Module({
|
|
524
|
+
imports: [
|
|
525
|
+
AuthModule.forRootAsync({
|
|
526
|
+
useFactory: () => ({
|
|
527
|
+
accessSecret: process.env.JWT_ACCESS_SECRET ?? 'dev-access-secret-change-me',
|
|
528
|
+
refreshSecret: process.env.JWT_REFRESH_SECRET ?? 'dev-refresh-secret-change-me',
|
|
529
|
+
accessTtl: '15m',
|
|
530
|
+
refreshTtl: '30d',
|
|
531
|
+
tokenHashPepper: process.env.TOKEN_HASH_PEPPER,
|
|
532
|
+
}),
|
|
533
|
+
store: { useClass: InMemoryRefreshTokenStore },
|
|
534
|
+
validator: { useClass: InMemoryUserValidator },
|
|
535
|
+
}),
|
|
536
|
+
],
|
|
537
|
+
controllers: [AuthController],
|
|
538
|
+
// APP_GUARD provided here applies globally: every route requires a valid
|
|
539
|
+
// access token unless decorated with @Public().
|
|
540
|
+
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }],
|
|
541
|
+
})
|
|
542
|
+
export class AuthIntegrationModule {}
|
|
543
|
+
`,
|
|
544
|
+
},
|
|
545
|
+
];
|
|
546
|
+
return {
|
|
547
|
+
files,
|
|
548
|
+
env: ENV,
|
|
549
|
+
integrationModule: 'AuthIntegrationModule',
|
|
550
|
+
integrationImportPath: './auth/auth-integration.module',
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
function buildPlan(pkg, store) {
|
|
554
|
+
return store === 'memory' ? memoryPlan(pkg) : mongoosePlan(pkg);
|
|
555
|
+
}
|
|
556
|
+
//# sourceMappingURL=templates.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"templates.js","sourceRoot":"","sources":["../../src/lib/cli/templates.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { EnvEntry } from './templates';
|
|
2
|
+
export declare const c: {
|
|
3
|
+
bold: (s: string) => string;
|
|
4
|
+
dim: (s: string) => string;
|
|
5
|
+
green: (s: string) => string;
|
|
6
|
+
yellow: (s: string) => string;
|
|
7
|
+
red: (s: string) => string;
|
|
8
|
+
cyan: (s: string) => string;
|
|
9
|
+
};
|
|
10
|
+
export declare const log: {
|
|
11
|
+
info: (m: string) => void;
|
|
12
|
+
step: (m: string) => void;
|
|
13
|
+
ok: (m: string) => void;
|
|
14
|
+
skip: (m: string) => void;
|
|
15
|
+
warn: (m: string) => void;
|
|
16
|
+
err: (m: string) => void;
|
|
17
|
+
};
|
|
18
|
+
export declare function readText(path: string): string;
|
|
19
|
+
export interface WriteOpts {
|
|
20
|
+
force?: boolean;
|
|
21
|
+
dryRun?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export type WriteResult = 'written' | 'skipped' | 'would-write';
|
|
24
|
+
/** Write a file, creating parent dirs. Won't clobber an existing file unless `force`. */
|
|
25
|
+
export declare function writeFileSafe(path: string, content: string, opts: WriteOpts): WriteResult;
|
|
26
|
+
/** Copy `path` to `path.bak` (or `.bak.1`, `.bak.2`, …) before mutating it. */
|
|
27
|
+
export declare function backup(path: string, dryRun?: boolean): string | null;
|
|
28
|
+
/** Append any missing keys to an env file. Returns the keys actually added. */
|
|
29
|
+
export declare function mergeEnvFile(path: string, entries: EnvEntry[], dryRun?: boolean): string[];
|
|
30
|
+
export interface ProjectInfo {
|
|
31
|
+
root: string;
|
|
32
|
+
srcDir: string;
|
|
33
|
+
appModulePath: string | null;
|
|
34
|
+
mainPath: string | null;
|
|
35
|
+
pkgJson: Record<string, any>;
|
|
36
|
+
}
|
|
37
|
+
export declare function loadProject(root: string): ProjectInfo | null;
|
|
38
|
+
export declare function isNestProject(p: ProjectInfo): boolean;
|
|
39
|
+
export type PkgManager = 'npm' | 'yarn' | 'pnpm';
|
|
40
|
+
export declare function detectPackageManager(root: string): PkgManager;
|
|
41
|
+
export declare function installCommand(pm: PkgManager, deps: string[], dev: boolean): {
|
|
42
|
+
cmd: string;
|
|
43
|
+
args: string[];
|
|
44
|
+
};
|
|
45
|
+
export declare function runInstall(root: string, pm: PkgManager, deps: string[], devDeps: string[]): boolean;
|
|
46
|
+
export type PatchResult = 'patched' | 'already' | 'manual';
|
|
47
|
+
/**
|
|
48
|
+
* Add the integration module to AppModule: an import statement plus an entry at
|
|
49
|
+
* the head of the `@Module({ imports: [ ... ] })` array. Backs up first.
|
|
50
|
+
*/
|
|
51
|
+
export declare function patchAppModule(path: string, moduleName: string, importPath: string, dryRun?: boolean): PatchResult;
|
|
52
|
+
/** Ensure main.ts installs a global ValidationPipe. Backs up first. */
|
|
53
|
+
export declare function patchMain(path: string, dryRun?: boolean): PatchResult;
|
|
54
|
+
export declare function resolveDir(input: string | undefined): string;
|
|
55
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/lib/cli/utils.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAQ5C,eAAO,MAAM,CAAC;cACF,MAAM;aACP,MAAM;eACJ,MAAM;gBACL,MAAM;aACT,MAAM;cACL,MAAM;CACjB,CAAC;AAEF,eAAO,MAAM,GAAG;cACJ,MAAM;cACN,MAAM;YACR,MAAM;cACJ,MAAM;cACN,MAAM;aACP,MAAM;CAChB,CAAC;AAIF,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE7C;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,aAAa,CAAC;AAEhE,yFAAyF;AACzF,wBAAgB,aAAa,CAC3B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,GACd,WAAW,CAOb;AAED,+EAA+E;AAC/E,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAOpE;AAID,+EAA+E;AAC/E,wBAAgB,YAAY,CAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,EAAE,EACnB,MAAM,CAAC,EAAE,OAAO,GACf,MAAM,EAAE,CAqBV;AAID,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAC9B;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAgB5D;AAED,wBAAgB,aAAa,CAAC,CAAC,EAAE,WAAW,GAAG,OAAO,CAGrD;AASD,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAI7D;AAED,wBAAgB,cAAc,CAC5B,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,MAAM,EAAE,EACd,GAAG,EAAE,OAAO,GACX;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAIjC;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,UAAU,EACd,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,MAAM,EAAE,GAChB,OAAO,CAoBT;AAmBD,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE3D;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,OAAO,GACf,WAAW,CA+Bb;AAED,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,GAAG,WAAW,CA4BrE;AAID,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAE5D"}
|