Skip to content

Commit ec1d3a2

Browse files
committed
Improves code organization and type safety
Alphabetizes imports and exports throughout the codebase for better maintainability and reducing merge conflicts. Replaces loose `any` types with proper TypeScript types, including introducing `EventEmitterLike` interface for event emitter dependencies and using `StringValue` from 'ms' package for JWT expiration values. Enhances type safety in request handlers by replacing generic `any` with specific interface types and removing unnecessary type assertions. Formats multi-line object properties consistently across configuration files.
1 parent 1ffaf5c commit ec1d3a2

57 files changed

Lines changed: 887 additions & 271 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Release
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
11+
permissions:
12+
contents: read
13+
id-token: write
14+
15+
steps:
16+
- name: Checkout code
17+
uses: actions/checkout@v6
18+
19+
- name: Install pnpm
20+
uses: pnpm/action-setup@v5
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v6
24+
with:
25+
node-version: 20
26+
registry-url: https://registry.npmjs.org
27+
cache: pnpm
28+
29+
- name: Install dependencies
30+
run: pnpm install --frozen-lockfile
31+
32+
- name: Build
33+
run: pnpm run build
34+
35+
- name: Publish to npm
36+
run: pnpm publish --access public --no-git-checks --provenance
37+
env:
38+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

.github/workflows/tests.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
node-version: [20, 22]
17+
18+
name: Node ${{ matrix.node-version }}
19+
20+
steps:
21+
- name: Checkout code
22+
uses: actions/checkout@v6
23+
24+
- name: Install pnpm
25+
uses: pnpm/action-setup@v5
26+
27+
- name: Setup Node.js ${{ matrix.node-version }}
28+
uses: actions/setup-node@v6
29+
with:
30+
node-version: ${{ matrix.node-version }}
31+
cache: pnpm
32+
33+
- name: Install dependencies
34+
run: pnpm install --frozen-lockfile
35+
36+
- name: Build
37+
run: pnpm run build
38+
39+
- name: Run tests
40+
run: pnpm test

README.md

Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
<p align="center">
2+
<h1 align="center">@nestbolt/authentication</h1>
3+
<p align="center">Frontend-agnostic authentication backend for NestJS</p>
4+
</p>
5+
6+
<p align="center">
7+
<a href="https://www.npmjs.com/package/@nestbolt/authentication"><img src="https://img.shields.io/npm/v/@nestbolt/authentication.svg" alt="NPM Version" /></a>
8+
<a href="https://www.npmjs.com/package/@nestbolt/authentication"><img src="https://img.shields.io/npm/dm/@nestbolt/authentication.svg" alt="NPM Downloads" /></a>
9+
<a href="https://github.com/nestbolt/authentication/actions"><img src="https://github.com/nestbolt/authentication/workflows/Tests/badge.svg" alt="Tests" /></a>
10+
<a href="https://github.com/nestbolt/authentication/blob/main/LICENSE.md"><img src="https://img.shields.io/npm/l/@nestbolt/authentication.svg" alt="License" /></a>
11+
</p>
12+
13+
---
14+
15+
A complete, database-agnostic authentication backend for NestJS with support for registration, login, password reset, email verification, profile management, password confirmation, and two-factor authentication (TOTP).
16+
17+
Inspired by [Laravel Fortify](https://github.com/laravel/fortify).
18+
19+
## Table of Contents
20+
21+
- [Installation](#installation)
22+
- [Quick Start](#quick-start)
23+
- [Module Configuration](#module-configuration)
24+
- [Features](#features)
25+
- [Database Adapters](#database-adapters)
26+
- [API Routes](#api-routes)
27+
- [Events](#events)
28+
- [Configuration Options](#configuration-options)
29+
- [Testing](#testing)
30+
- [Changelog](#changelog)
31+
- [Contributing](#contributing)
32+
- [License](#license)
33+
34+
## Installation
35+
36+
```bash
37+
# pnpm
38+
pnpm add @nestbolt/authentication
39+
40+
# npm
41+
npm install @nestbolt/authentication
42+
43+
# yarn
44+
yarn add @nestbolt/authentication
45+
```
46+
47+
### Peer Dependencies
48+
49+
```bash
50+
pnpm add @nestjs/passport @nestjs/jwt passport passport-jwt passport-local class-validator class-transformer reflect-metadata
51+
```
52+
53+
Optional:
54+
```bash
55+
pnpm add @nestjs/event-emitter @nestjs/throttler
56+
```
57+
58+
## Quick Start
59+
60+
1. **Implement the `UserRepository` interface** for your database:
61+
62+
```typescript
63+
import { Injectable } from "@nestjs/common";
64+
import { UserRepository, AuthUser } from "@nestbolt/authentication";
65+
66+
@Injectable()
67+
export class MyUserRepository implements UserRepository {
68+
async findById(id: string): Promise<AuthUser | null> { /* ... */ }
69+
async findByField(field: string, value: string): Promise<AuthUser | null> { /* ... */ }
70+
async save(user: Partial<AuthUser> & { id: string }): Promise<AuthUser> { /* ... */ }
71+
async create(data: Omit<AuthUser, "id">): Promise<AuthUser> { /* ... */ }
72+
}
73+
```
74+
75+
2. **Import `AuthenticationModule`** in your app module:
76+
77+
```typescript
78+
import { AuthenticationModule, Feature } from "@nestbolt/authentication";
79+
import { MyUserRepository } from "./my-user.repository";
80+
81+
@Module({
82+
imports: [
83+
AuthenticationModule.forRoot({
84+
features: [
85+
Feature.REGISTRATION,
86+
Feature.RESET_PASSWORDS,
87+
Feature.EMAIL_VERIFICATION,
88+
Feature.UPDATE_PROFILE_INFORMATION,
89+
Feature.UPDATE_PASSWORDS,
90+
Feature.TWO_FACTOR_AUTHENTICATION,
91+
],
92+
userRepository: MyUserRepository,
93+
jwtSecret: process.env.JWT_SECRET!,
94+
refreshSecret: process.env.REFRESH_SECRET!,
95+
encryptionKey: process.env.ENCRYPTION_KEY!, // 32-byte base64
96+
appName: "MyApp",
97+
}),
98+
],
99+
})
100+
export class AppModule {}
101+
```
102+
103+
3. **That's it!** All 17 auth routes are now available.
104+
105+
## Module Configuration
106+
107+
### Synchronous
108+
109+
```typescript
110+
AuthenticationModule.forRoot({
111+
features: [Feature.REGISTRATION, Feature.TWO_FACTOR_AUTHENTICATION],
112+
userRepository: TypeOrmUserRepository,
113+
passwordResetRepository: TypeOrmPasswordResetRepository,
114+
jwtSecret: "your-jwt-secret",
115+
refreshSecret: "your-refresh-secret",
116+
encryptionKey: "base64-encoded-32-byte-key",
117+
appName: "MyApp",
118+
});
119+
```
120+
121+
### Asynchronous
122+
123+
```typescript
124+
AuthenticationModule.forRootAsync({
125+
imports: [ConfigModule],
126+
inject: [ConfigService],
127+
useFactory: (config: ConfigService) => ({
128+
features: [Feature.REGISTRATION, Feature.TWO_FACTOR_AUTHENTICATION],
129+
userRepository: TypeOrmUserRepository,
130+
jwtSecret: config.get("JWT_SECRET"),
131+
refreshSecret: config.get("REFRESH_SECRET"),
132+
encryptionKey: config.get("ENCRYPTION_KEY"),
133+
}),
134+
});
135+
```
136+
137+
## Features
138+
139+
Enable or disable features via the `features` array:
140+
141+
| Feature | Description |
142+
|---------|-------------|
143+
| `Feature.REGISTRATION` | User registration (POST /register) |
144+
| `Feature.RESET_PASSWORDS` | Password reset flow (POST /forgot-password, POST /reset-password) |
145+
| `Feature.EMAIL_VERIFICATION` | Email verification (GET /email/verify/:id/:hash) |
146+
| `Feature.UPDATE_PROFILE_INFORMATION` | Profile updates (PUT /user/profile-information) |
147+
| `Feature.UPDATE_PASSWORDS` | Password updates (PUT /user/password) |
148+
| `Feature.TWO_FACTOR_AUTHENTICATION` | Full 2FA with TOTP, QR codes, and recovery codes |
149+
150+
## Database Adapters
151+
152+
The package is **database-agnostic**. Implement `UserRepository` and optionally `PasswordResetRepository` for any database:
153+
154+
### TypeORM (SQL)
155+
```typescript
156+
@Injectable()
157+
export class TypeOrmUserRepository implements UserRepository {
158+
constructor(@InjectRepository(User) private repo: Repository<User>) {}
159+
findById(id: string) { return this.repo.findOneBy({ id }); }
160+
findByField(field: string, value: string) { return this.repo.findOneBy({ [field]: value }); }
161+
save(user) { return this.repo.save(user); }
162+
create(data) { return this.repo.save(this.repo.create(data)); }
163+
}
164+
```
165+
166+
### Mongoose (MongoDB)
167+
```typescript
168+
@Injectable()
169+
export class MongooseUserRepository implements UserRepository {
170+
constructor(@InjectModel(User.name) private model: Model<UserDocument>) {}
171+
findById(id: string) { return this.model.findById(id).lean().exec(); }
172+
findByField(field: string, value: string) { return this.model.findOne({ [field]: value }).lean().exec(); }
173+
save(user) { return this.model.findByIdAndUpdate(user.id, user, { new: true }).lean().exec(); }
174+
create(data) { return this.model.create(data); }
175+
}
176+
```
177+
178+
### Prisma, MikroORM, DynamoDB, etc.
179+
Same pattern - implement the interface for your ORM/driver.
180+
181+
## API Routes
182+
183+
| Method | Route | Description | Auth |
184+
|--------|-------|-------------|------|
185+
| POST | /login | Authenticate user | No |
186+
| POST | /logout | Log out | JWT |
187+
| POST | /register | Create new user | No |
188+
| POST | /forgot-password | Send reset link | No |
189+
| POST | /reset-password | Reset password | No |
190+
| GET | /email/verify/:id/:hash | Verify email | JWT |
191+
| POST | /email/verification-notification | Resend verification | JWT |
192+
| PUT | /user/profile-information | Update profile | JWT |
193+
| PUT | /user/password | Change password | JWT |
194+
| POST | /user/confirm-password | Confirm password | JWT |
195+
| GET | /user/confirmed-password-status | Check confirmation | JWT |
196+
| POST | /user/two-factor-authentication | Enable 2FA | JWT |
197+
| DELETE | /user/two-factor-authentication | Disable 2FA | JWT |
198+
| POST | /user/confirmed-two-factor-authentication | Confirm 2FA setup | JWT |
199+
| GET | /user/two-factor-qr-code | Get QR code SVG | JWT |
200+
| GET | /user/two-factor-secret-key | Get TOTP secret | JWT |
201+
| GET/POST | /user/two-factor-recovery-codes | Get/regenerate codes | JWT |
202+
| POST | /two-factor-challenge | Complete 2FA login | No |
203+
204+
## Events
205+
206+
Subscribe to authentication events using `@nestjs/event-emitter`:
207+
208+
```typescript
209+
import { OnEvent } from "@nestjs/event-emitter";
210+
import { AUTH_EVENTS, UserEvent } from "@nestbolt/authentication";
211+
212+
@Injectable()
213+
export class AuthListener {
214+
@OnEvent(AUTH_EVENTS.LOGIN)
215+
handleLogin(payload: UserEvent) {
216+
console.log(`User ${payload.user.email} logged in`);
217+
}
218+
}
219+
```
220+
221+
Available events: `auth.login`, `auth.logout`, `auth.registered`, `auth.lockout`, `auth.password-reset`, `auth.password-updated`, `auth.email-verified`, `auth.two-factor-enabled`, `auth.two-factor-disabled`, `auth.two-factor-confirmed`, `auth.two-factor-challenged`, `auth.two-factor-failed`, `auth.valid-two-factor-code`, `auth.recovery-code-replaced`, `auth.recovery-codes-generated`
222+
223+
## Configuration Options
224+
225+
| Option | Type | Default | Description |
226+
|--------|------|---------|-------------|
227+
| `features` | `Feature[]` | *required* | Enabled features |
228+
| `userRepository` | `Type<UserRepository>` | *required* | User repository class |
229+
| `passwordResetRepository` | `Type<PasswordResetRepository>` | - | Password reset token storage |
230+
| `jwtSecret` | `string` | *required* | JWT signing secret |
231+
| `refreshSecret` | `string` | *required* | Refresh token secret |
232+
| `encryptionKey` | `string` | *required* | 32-byte base64 key for 2FA encryption |
233+
| `jwtExpiresIn` | `string` | `"15m"` | Access token TTL |
234+
| `refreshExpiresIn` | `string` | `"7d"` | Refresh token TTL |
235+
| `usernameField` | `string` | `"email"` | Login username field |
236+
| `lowercaseUsernames` | `boolean` | `true` | Lowercase usernames on login |
237+
| `loginRateLimit` | `{ ttl, limit }` | `{ 60000, 5 }` | Login rate limiting |
238+
| `passwordTimeout` | `number` | `900` | Password confirmation timeout (seconds) |
239+
| `appName` | `string` | `"NestBolt"` | App name for TOTP QR codes |
240+
| `twoFactorOptions.confirm` | `boolean` | `false` | Require 2FA confirmation step |
241+
| `twoFactorOptions.confirmPassword` | `boolean` | `false` | Require password before 2FA changes |
242+
| `twoFactorOptions.window` | `number` | `1` | TOTP time window |
243+
| `twoFactorOptions.secretLength` | `number` | `20` | TOTP secret key length |
244+
245+
## Testing
246+
247+
```bash
248+
pnpm test # Run tests
249+
pnpm test:watch # Watch mode
250+
pnpm test:cov # Coverage report
251+
```
252+
253+
## Changelog
254+
255+
See [CHANGELOG.md](CHANGELOG.md).
256+
257+
## Contributing
258+
259+
See [CONTRIBUTING.md](CONTRIBUTING.md).
260+
261+
## Security
262+
263+
For security-related issues, please use the **security** label on [GitHub Issues](https://github.com/nestbolt/authentication/issues).
264+
265+
## Credits
266+
267+
- Inspired by [Laravel Fortify](https://github.com/laravel/fortify) by Taylor Otwell
268+
269+
## License
270+
271+
[MIT License](LICENSE.md)

0 commit comments

Comments
 (0)