@lenan-soft/auth 1.0.1
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/README.md +591 -0
- package/dist/index.cjs +45 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +129 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/nestjs/index.cjs +738 -0
- package/dist/nestjs/index.cjs.map +1 -0
- package/dist/nestjs/index.d.cts +510 -0
- package/dist/nestjs/index.d.ts +510 -0
- package/dist/nestjs/index.js +709 -0
- package/dist/nestjs/index.js.map +1 -0
- package/dist/react/index.cjs +560 -0
- package/dist/react/index.cjs.map +1 -0
- package/dist/react/index.d.cts +285 -0
- package/dist/react/index.d.ts +285 -0
- package/dist/react/index.js +531 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/native/index.cjs +60 -0
- package/dist/react/native/index.cjs.map +1 -0
- package/dist/react/native/index.d.cts +50 -0
- package/dist/react/native/index.d.ts +50 -0
- package/dist/react/native/index.js +41 -0
- package/dist/react/native/index.js.map +1 -0
- package/dist/shared/index.cjs +45 -0
- package/dist/shared/index.cjs.map +1 -0
- package/dist/shared/index.d.cts +129 -0
- package/dist/shared/index.d.ts +129 -0
- package/dist/shared/index.js +17 -0
- package/dist/shared/index.js.map +1 -0
- package/package.json +151 -0
package/README.md
ADDED
|
@@ -0,0 +1,591 @@
|
|
|
1
|
+
# @lenan-soft/auth
|
|
2
|
+
|
|
3
|
+
A reusable JWT authentication library for NestJS and React/React Native applications.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✅ JWT access + refresh token authentication
|
|
8
|
+
- ✅ NestJS dynamic module with guards, strategies, and decorators
|
|
9
|
+
- ✅ React hooks and context provider
|
|
10
|
+
- ✅ React Native support with Expo SecureStore adapter
|
|
11
|
+
- ✅ Interface-driven design — bring your own user entity
|
|
12
|
+
- ✅ TypeScript first with full type definitions
|
|
13
|
+
- ✅ Automatic token refresh on 401 responses
|
|
14
|
+
- ✅ Bcrypt password hashing
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install @lenan-soft/auth
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Peer Dependencies
|
|
23
|
+
|
|
24
|
+
**For NestJS:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install @nestjs/common @nestjs/core @nestjs/jwt @nestjs/passport passport passport-jwt class-validator class-transformer reflect-metadata
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**For React:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install react
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**For React Native (Expo):**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx expo install expo-secure-store
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
### NestJS Backend
|
|
45
|
+
|
|
46
|
+
#### 1. Implement the User Service Interface
|
|
47
|
+
|
|
48
|
+
Create a service that implements `UserServiceInterface`:
|
|
49
|
+
|
|
50
|
+
```typescript
|
|
51
|
+
// user.service.ts
|
|
52
|
+
import { Injectable } from "@nestjs/common";
|
|
53
|
+
import { InjectRepository } from "@nestjs/typeorm";
|
|
54
|
+
import { Repository } from "typeorm";
|
|
55
|
+
import { UserServiceInterface, AuthUser } from "@lenan-soft/auth/nestjs";
|
|
56
|
+
import { User } from "./user.entity";
|
|
57
|
+
|
|
58
|
+
@Injectable()
|
|
59
|
+
export class UserService implements UserServiceInterface<User> {
|
|
60
|
+
constructor(
|
|
61
|
+
@InjectRepository(User)
|
|
62
|
+
private readonly userRepository: Repository<User>,
|
|
63
|
+
) {}
|
|
64
|
+
|
|
65
|
+
async findByEmail(email: string): Promise<User | null> {
|
|
66
|
+
return this.userRepository.findOne({ where: { email } });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async findById(id: string): Promise<User | null> {
|
|
70
|
+
return this.userRepository.findOne({ where: { id } });
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async createUser(data: {
|
|
74
|
+
email: string;
|
|
75
|
+
passwordHash: string;
|
|
76
|
+
}): Promise<User> {
|
|
77
|
+
const user = this.userRepository.create({
|
|
78
|
+
email: data.email,
|
|
79
|
+
passwordHash: data.passwordHash,
|
|
80
|
+
hashedRefreshToken: null,
|
|
81
|
+
});
|
|
82
|
+
return this.userRepository.save(user);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async updateRefreshToken(
|
|
86
|
+
userId: string,
|
|
87
|
+
hashedToken: string | null,
|
|
88
|
+
): Promise<void> {
|
|
89
|
+
await this.userRepository.update(userId, {
|
|
90
|
+
hashedRefreshToken: hashedToken,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async validateRefreshToken(
|
|
95
|
+
userId: string,
|
|
96
|
+
hashedToken: string,
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
const user = await this.findById(userId);
|
|
99
|
+
return user?.hashedRefreshToken === hashedToken;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### 2. Create Your User Entity
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
// user.entity.ts
|
|
108
|
+
import {
|
|
109
|
+
Entity,
|
|
110
|
+
PrimaryGeneratedColumn,
|
|
111
|
+
Column,
|
|
112
|
+
CreateDateColumn,
|
|
113
|
+
UpdateDateColumn,
|
|
114
|
+
} from "typeorm";
|
|
115
|
+
import { AuthUser } from "@lenan-soft/auth/nestjs";
|
|
116
|
+
|
|
117
|
+
@Entity("users")
|
|
118
|
+
export class User implements AuthUser {
|
|
119
|
+
@PrimaryGeneratedColumn("uuid")
|
|
120
|
+
id!: string;
|
|
121
|
+
|
|
122
|
+
@Column({ unique: true })
|
|
123
|
+
email!: string;
|
|
124
|
+
|
|
125
|
+
@Column()
|
|
126
|
+
passwordHash!: string;
|
|
127
|
+
|
|
128
|
+
@Column({ nullable: true })
|
|
129
|
+
hashedRefreshToken!: string | null;
|
|
130
|
+
|
|
131
|
+
// Add your own custom fields
|
|
132
|
+
@Column({ nullable: true })
|
|
133
|
+
firstName?: string;
|
|
134
|
+
|
|
135
|
+
@Column({ nullable: true })
|
|
136
|
+
lastName?: string;
|
|
137
|
+
|
|
138
|
+
@CreateDateColumn()
|
|
139
|
+
createdAt!: Date;
|
|
140
|
+
|
|
141
|
+
@UpdateDateColumn()
|
|
142
|
+
updatedAt!: Date;
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
#### 3. Register the Auth Module
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
// app.module.ts
|
|
150
|
+
import { Module } from "@nestjs/common";
|
|
151
|
+
import { ConfigModule, ConfigService } from "@nestjs/config";
|
|
152
|
+
import { TypeOrmModule } from "@nestjs/typeorm";
|
|
153
|
+
import { LenanAuthModule } from "@lenan-soft/auth/nestjs";
|
|
154
|
+
import { UserService } from "./user/user.service";
|
|
155
|
+
import { User } from "./user/user.entity";
|
|
156
|
+
|
|
157
|
+
@Module({
|
|
158
|
+
imports: [
|
|
159
|
+
ConfigModule.forRoot({ isGlobal: true }),
|
|
160
|
+
TypeOrmModule.forRoot({
|
|
161
|
+
// your database config
|
|
162
|
+
}),
|
|
163
|
+
TypeOrmModule.forFeature([User]),
|
|
164
|
+
|
|
165
|
+
// Register auth module with async configuration
|
|
166
|
+
LenanAuthModule.registerAsync(
|
|
167
|
+
{
|
|
168
|
+
imports: [ConfigModule],
|
|
169
|
+
useFactory: (config: ConfigService) => ({
|
|
170
|
+
jwtSecret: config.getOrThrow("JWT_SECRET"),
|
|
171
|
+
jwtAccessExpiry: config.get("JWT_ACCESS_EXPIRY", "15m"),
|
|
172
|
+
jwtRefreshExpiry: config.get("JWT_REFRESH_EXPIRY", "7d"),
|
|
173
|
+
jwtRefreshSecret: config.get("JWT_REFRESH_SECRET"),
|
|
174
|
+
}),
|
|
175
|
+
inject: [ConfigService],
|
|
176
|
+
},
|
|
177
|
+
UserService, // Your UserService class
|
|
178
|
+
),
|
|
179
|
+
],
|
|
180
|
+
providers: [UserService],
|
|
181
|
+
})
|
|
182
|
+
export class AppModule {}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### 4. Protect Routes
|
|
186
|
+
|
|
187
|
+
```typescript
|
|
188
|
+
// profile.controller.ts
|
|
189
|
+
import { Controller, Get, UseGuards } from "@nestjs/common";
|
|
190
|
+
import { JwtAuthGuard, CurrentUser, Public } from "@lenan-soft/auth/nestjs";
|
|
191
|
+
|
|
192
|
+
@Controller("profile")
|
|
193
|
+
@UseGuards(JwtAuthGuard) // Protect all routes in this controller
|
|
194
|
+
export class ProfileController {
|
|
195
|
+
@Get()
|
|
196
|
+
getProfile(@CurrentUser() user: User) {
|
|
197
|
+
return { user };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
@Public() // This route is accessible without authentication
|
|
201
|
+
@Get("public")
|
|
202
|
+
getPublicInfo() {
|
|
203
|
+
return { message: "This is public" };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### 5. Apply Guard Globally (Optional)
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
// main.ts
|
|
212
|
+
import { NestFactory, Reflector } from "@nestjs/core";
|
|
213
|
+
import { JwtAuthGuard } from "@lenan-soft/auth/nestjs";
|
|
214
|
+
import { AppModule } from "./app.module";
|
|
215
|
+
|
|
216
|
+
async function bootstrap() {
|
|
217
|
+
const app = await NestFactory.create(AppModule);
|
|
218
|
+
|
|
219
|
+
// Apply JWT guard globally
|
|
220
|
+
const reflector = app.get(Reflector);
|
|
221
|
+
app.useGlobalGuards(new JwtAuthGuard(reflector));
|
|
222
|
+
|
|
223
|
+
await app.listen(3000);
|
|
224
|
+
}
|
|
225
|
+
bootstrap();
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### React Frontend
|
|
229
|
+
|
|
230
|
+
```tsx
|
|
231
|
+
// App.tsx
|
|
232
|
+
import { AuthProvider } from "@lenan-soft/auth/react";
|
|
233
|
+
|
|
234
|
+
function App() {
|
|
235
|
+
return (
|
|
236
|
+
<AuthProvider baseUrl="https://api.example.com">
|
|
237
|
+
<Router />
|
|
238
|
+
</AuthProvider>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
#### Using Auth Hooks
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
// LoginForm.tsx
|
|
247
|
+
import { useAuth } from "@lenan-soft/auth/react";
|
|
248
|
+
|
|
249
|
+
function LoginForm() {
|
|
250
|
+
const { login, isLoading, error } = useAuth();
|
|
251
|
+
const [email, setEmail] = useState("");
|
|
252
|
+
const [password, setPassword] = useState("");
|
|
253
|
+
|
|
254
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
try {
|
|
257
|
+
await login(email, password);
|
|
258
|
+
// Redirect to dashboard
|
|
259
|
+
} catch {
|
|
260
|
+
// Error is available via `error` state
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
return (
|
|
265
|
+
<form onSubmit={handleSubmit}>
|
|
266
|
+
{error && <div className="error">{error}</div>}
|
|
267
|
+
<input
|
|
268
|
+
type="email"
|
|
269
|
+
value={email}
|
|
270
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
271
|
+
placeholder="Email"
|
|
272
|
+
/>
|
|
273
|
+
<input
|
|
274
|
+
type="password"
|
|
275
|
+
value={password}
|
|
276
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
277
|
+
placeholder="Password"
|
|
278
|
+
/>
|
|
279
|
+
<button type="submit" disabled={isLoading}>
|
|
280
|
+
{isLoading ? "Logging in..." : "Login"}
|
|
281
|
+
</button>
|
|
282
|
+
</form>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### Protected Routes
|
|
288
|
+
|
|
289
|
+
```tsx
|
|
290
|
+
// ProtectedRoute.tsx
|
|
291
|
+
import { Navigate } from "react-router-dom";
|
|
292
|
+
import { useAuth } from "@lenan-soft/auth/react";
|
|
293
|
+
|
|
294
|
+
function ProtectedRoute({ children }: { children: React.ReactNode }) {
|
|
295
|
+
const { isAuthenticated, isLoading } = useAuth();
|
|
296
|
+
|
|
297
|
+
if (isLoading) {
|
|
298
|
+
return <div>Loading...</div>;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!isAuthenticated) {
|
|
302
|
+
return <Navigate to="/login" />;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return <>{children}</>;
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
#### Display User Info
|
|
310
|
+
|
|
311
|
+
```tsx
|
|
312
|
+
// UserProfile.tsx
|
|
313
|
+
import { useUser } from "@lenan-soft/auth/react";
|
|
314
|
+
|
|
315
|
+
function UserProfile() {
|
|
316
|
+
const user = useUser();
|
|
317
|
+
|
|
318
|
+
if (!user) {
|
|
319
|
+
return null;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return (
|
|
323
|
+
<div>
|
|
324
|
+
<h2>Welcome, {user.email}!</h2>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### React Native with Expo
|
|
331
|
+
|
|
332
|
+
```tsx
|
|
333
|
+
// App.tsx
|
|
334
|
+
import { AuthProvider } from "@lenan-soft/auth/react";
|
|
335
|
+
import { SecureStoreAdapter } from "@lenan-soft/auth/react/native";
|
|
336
|
+
|
|
337
|
+
const tokenStorage = new SecureStoreAdapter();
|
|
338
|
+
|
|
339
|
+
export default function App() {
|
|
340
|
+
return (
|
|
341
|
+
<AuthProvider baseUrl="https://api.example.com" tokenStorage={tokenStorage}>
|
|
342
|
+
<RootNavigator />
|
|
343
|
+
</AuthProvider>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
## API Reference
|
|
349
|
+
|
|
350
|
+
### NestJS Module
|
|
351
|
+
|
|
352
|
+
#### LenanAuthModule
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
// Sync registration
|
|
356
|
+
LenanAuthModule.register(options: AuthModuleOptions, userService: Type<UserServiceInterface>)
|
|
357
|
+
|
|
358
|
+
// Async registration
|
|
359
|
+
LenanAuthModule.registerAsync(options: AuthModuleAsyncOptions, userService: Type<UserServiceInterface>)
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
#### AuthModuleOptions
|
|
363
|
+
|
|
364
|
+
| Option | Type | Default | Description |
|
|
365
|
+
| ------------------ | --------- | ------------------------ | ---------------------------------------------- |
|
|
366
|
+
| `jwtSecret` | `string` | required | Secret for signing JWT access tokens |
|
|
367
|
+
| `jwtAccessExpiry` | `string` | `'15m'` | Access token expiration |
|
|
368
|
+
| `jwtRefreshExpiry` | `string` | `'7d'` | Refresh token expiration |
|
|
369
|
+
| `jwtRefreshSecret` | `string` | `jwtSecret + '_refresh'` | Separate secret for refresh tokens |
|
|
370
|
+
| `useController` | `boolean` | `true` | Whether to register the default AuthController |
|
|
371
|
+
|
|
372
|
+
#### UserServiceInterface
|
|
373
|
+
|
|
374
|
+
```typescript
|
|
375
|
+
interface UserServiceInterface<TUser extends AuthUser = AuthUser> {
|
|
376
|
+
findByEmail(email: string): Promise<TUser | null>;
|
|
377
|
+
findById(id: string): Promise<TUser | null>;
|
|
378
|
+
createUser(data: { email: string; passwordHash: string }): Promise<TUser>;
|
|
379
|
+
updateRefreshToken(userId: string, hashedToken: string | null): Promise<void>;
|
|
380
|
+
validateRefreshToken(userId: string, hashedToken: string): Promise<boolean>;
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
#### Decorators
|
|
385
|
+
|
|
386
|
+
| Decorator | Description |
|
|
387
|
+
| -------------------- | --------------------------------------- |
|
|
388
|
+
| `@CurrentUser()` | Extract current user from request |
|
|
389
|
+
| `@CurrentUser('id')` | Extract specific user property |
|
|
390
|
+
| `@Public()` | Mark route as public (no auth required) |
|
|
391
|
+
|
|
392
|
+
#### Guards
|
|
393
|
+
|
|
394
|
+
| Guard | Description |
|
|
395
|
+
| ------------------- | --------------------------------------------- |
|
|
396
|
+
| `JwtAuthGuard` | Validates access tokens, respects `@Public()` |
|
|
397
|
+
| `RefreshTokenGuard` | Validates refresh tokens |
|
|
398
|
+
|
|
399
|
+
#### Services
|
|
400
|
+
|
|
401
|
+
| Service | Description |
|
|
402
|
+
| ----------------- | ---------------------------------------------------------- |
|
|
403
|
+
| `AuthService` | Main auth orchestration (login, register, refresh, logout) |
|
|
404
|
+
| `PasswordService` | Password hashing and comparison |
|
|
405
|
+
| `JwtTokenService` | Token generation and verification |
|
|
406
|
+
|
|
407
|
+
### React Package
|
|
408
|
+
|
|
409
|
+
#### AuthProvider Props
|
|
410
|
+
|
|
411
|
+
| Prop | Type | Default | Description |
|
|
412
|
+
| ------------------- | ------------------------ | --------------------- | ----------------------------- |
|
|
413
|
+
| `baseUrl` | `string` | required | Base URL for auth API |
|
|
414
|
+
| `tokenStorage` | `TokenStorage` | `LocalStorageAdapter` | Token storage implementation |
|
|
415
|
+
| `endpoints` | `Partial<AuthEndpoints>` | defaults | Custom endpoint paths |
|
|
416
|
+
| `headers` | `Record<string, string>` | `{}` | Custom headers for requests |
|
|
417
|
+
| `autoRefresh` | `boolean` | `true` | Auto-refresh on mount |
|
|
418
|
+
| `onAuthStateChange` | `(state) => void` | - | Callback on auth state change |
|
|
419
|
+
|
|
420
|
+
#### useAuth Hook
|
|
421
|
+
|
|
422
|
+
Returns:
|
|
423
|
+
|
|
424
|
+
| Property | Type | Description |
|
|
425
|
+
| --------------------------------------------- | -------------------- | -------------------- |
|
|
426
|
+
| `user` | `TUser \| null` | Current user |
|
|
427
|
+
| `tokens` | `AuthTokens \| null` | Current tokens |
|
|
428
|
+
| `isLoading` | `boolean` | Loading state |
|
|
429
|
+
| `isAuthenticated` | `boolean` | Auth status |
|
|
430
|
+
| `error` | `string \| null` | Error message |
|
|
431
|
+
| `login(email, password)` | `Promise<void>` | Login function |
|
|
432
|
+
| `register(email, password, confirmPassword?)` | `Promise<void>` | Register function |
|
|
433
|
+
| `logout()` | `Promise<void>` | Logout function |
|
|
434
|
+
| `refreshToken()` | `Promise<void>` | Manual token refresh |
|
|
435
|
+
| `clearError()` | `() => void` | Clear error state |
|
|
436
|
+
| `client` | `AuthClient` | Direct client access |
|
|
437
|
+
|
|
438
|
+
#### useUser Hook
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
const user = useUser<MyUserType>();
|
|
442
|
+
// Returns TUser | null
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
#### useIsAuthenticated Hook
|
|
446
|
+
|
|
447
|
+
```typescript
|
|
448
|
+
const isAuthenticated = useIsAuthenticated();
|
|
449
|
+
// Returns boolean (true only when authenticated AND not loading)
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### Built-in Auth Endpoints
|
|
453
|
+
|
|
454
|
+
The default `AuthController` provides:
|
|
455
|
+
|
|
456
|
+
| Method | Endpoint | Description |
|
|
457
|
+
| ------ | ---------------- | -------------------------------- |
|
|
458
|
+
| POST | `/auth/register` | Register new user |
|
|
459
|
+
| POST | `/auth/login` | Login with email/password |
|
|
460
|
+
| POST | `/auth/refresh` | Refresh tokens |
|
|
461
|
+
| POST | `/auth/logout` | Logout (requires auth) |
|
|
462
|
+
| GET | `/auth/me` | Get current user (requires auth) |
|
|
463
|
+
|
|
464
|
+
## Token Storage Adapters
|
|
465
|
+
|
|
466
|
+
### LocalStorageAdapter (Web)
|
|
467
|
+
|
|
468
|
+
Default for web browsers. Falls back to memory storage if localStorage is unavailable.
|
|
469
|
+
|
|
470
|
+
```typescript
|
|
471
|
+
import { LocalStorageAdapter } from "@lenan-soft/auth/react";
|
|
472
|
+
const storage = new LocalStorageAdapter();
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
### MemoryStorageAdapter (Testing/SSR)
|
|
476
|
+
|
|
477
|
+
In-memory storage for testing or server-side rendering.
|
|
478
|
+
|
|
479
|
+
```typescript
|
|
480
|
+
import { MemoryStorageAdapter } from "@lenan-soft/auth/react";
|
|
481
|
+
const storage = new MemoryStorageAdapter();
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
### SecureStoreAdapter (React Native)
|
|
485
|
+
|
|
486
|
+
Secure storage using Expo SecureStore.
|
|
487
|
+
|
|
488
|
+
```typescript
|
|
489
|
+
import { SecureStoreAdapter } from "@lenan-soft/auth/react/native";
|
|
490
|
+
const storage = new SecureStoreAdapter();
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Custom Storage
|
|
494
|
+
|
|
495
|
+
Implement the `TokenStorage` interface:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import type { TokenStorage } from "@lenan-soft/auth/shared";
|
|
499
|
+
|
|
500
|
+
class MyCustomStorage implements TokenStorage {
|
|
501
|
+
async getItem(key: string): Promise<string | null> {
|
|
502
|
+
// Your implementation
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
async setItem(key: string, value: string): Promise<void> {
|
|
506
|
+
// Your implementation
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async removeItem(key: string): Promise<void> {
|
|
510
|
+
// Your implementation
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
## Extending the User Entity
|
|
516
|
+
|
|
517
|
+
The library uses an interface-driven approach. Your user entity must implement `AuthUser`:
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
interface AuthUser {
|
|
521
|
+
id: string;
|
|
522
|
+
email: string;
|
|
523
|
+
passwordHash: string;
|
|
524
|
+
hashedRefreshToken: string | null;
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Add any additional fields you need:
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// user.entity.ts
|
|
532
|
+
@Entity("users")
|
|
533
|
+
export class User implements AuthUser {
|
|
534
|
+
@PrimaryGeneratedColumn("uuid")
|
|
535
|
+
id!: string;
|
|
536
|
+
|
|
537
|
+
@Column({ unique: true })
|
|
538
|
+
email!: string;
|
|
539
|
+
|
|
540
|
+
@Column()
|
|
541
|
+
passwordHash!: string;
|
|
542
|
+
|
|
543
|
+
@Column({ nullable: true })
|
|
544
|
+
hashedRefreshToken!: string | null;
|
|
545
|
+
|
|
546
|
+
// Your custom fields
|
|
547
|
+
@Column({ nullable: true })
|
|
548
|
+
avatarUrl?: string;
|
|
549
|
+
|
|
550
|
+
@Column({ default: "user" })
|
|
551
|
+
role!: "user" | "admin";
|
|
552
|
+
|
|
553
|
+
@Column({ default: false })
|
|
554
|
+
emailVerified!: boolean;
|
|
555
|
+
}
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
## Security Considerations
|
|
559
|
+
|
|
560
|
+
1. **JWT Secrets**: Use strong, unique secrets for production. Consider separate secrets for access and refresh tokens.
|
|
561
|
+
|
|
562
|
+
2. **Token Expiry**: Keep access tokens short-lived (15m default). Refresh tokens can be longer (7d default).
|
|
563
|
+
|
|
564
|
+
3. **HTTPS**: Always use HTTPS in production to protect tokens in transit.
|
|
565
|
+
|
|
566
|
+
4. **Refresh Token Rotation**: The library rotates refresh tokens on each refresh, invalidating the old token.
|
|
567
|
+
|
|
568
|
+
5. **Password Requirements**: Default validation requires 8+ characters with uppercase, lowercase, and numbers.
|
|
569
|
+
|
|
570
|
+
## Development
|
|
571
|
+
|
|
572
|
+
```bash
|
|
573
|
+
# Install dependencies
|
|
574
|
+
yarn install
|
|
575
|
+
|
|
576
|
+
# Build
|
|
577
|
+
yarn build
|
|
578
|
+
|
|
579
|
+
# Run tests
|
|
580
|
+
yarn test
|
|
581
|
+
|
|
582
|
+
# Run tests with coverage
|
|
583
|
+
yarn test:coverage
|
|
584
|
+
|
|
585
|
+
# Lint
|
|
586
|
+
yarn lint
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
## License
|
|
590
|
+
|
|
591
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
DEFAULT_AUTH_ENDPOINTS: () => DEFAULT_AUTH_ENDPOINTS,
|
|
24
|
+
TOKEN_STORAGE_KEYS: () => TOKEN_STORAGE_KEYS
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/shared/types.ts
|
|
29
|
+
var DEFAULT_AUTH_ENDPOINTS = {
|
|
30
|
+
login: "/auth/login",
|
|
31
|
+
register: "/auth/register",
|
|
32
|
+
refresh: "/auth/refresh",
|
|
33
|
+
logout: "/auth/logout",
|
|
34
|
+
me: "/auth/me"
|
|
35
|
+
};
|
|
36
|
+
var TOKEN_STORAGE_KEYS = {
|
|
37
|
+
ACCESS_TOKEN: "lenan_access_token",
|
|
38
|
+
REFRESH_TOKEN: "lenan_refresh_token"
|
|
39
|
+
};
|
|
40
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
41
|
+
0 && (module.exports = {
|
|
42
|
+
DEFAULT_AUTH_ENDPOINTS,
|
|
43
|
+
TOKEN_STORAGE_KEYS
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/shared/types.ts"],"sourcesContent":["// Re-export shared types from the main entry point\nexport * from \"./shared\";\n","/**\n * JWT payload structure\n */\nexport interface JwtPayload {\n /** User ID (subject) */\n sub: string;\n /** User email */\n email: string;\n /** Issued at timestamp */\n iat?: number;\n /** Expiration timestamp */\n exp?: number;\n}\n\n/**\n * Token response containing access and refresh tokens\n */\nexport interface AuthTokens {\n /** Short-lived access token */\n accessToken: string;\n /** Long-lived refresh token */\n refreshToken: string;\n}\n\n/**\n * Base user interface - minimal fields required by the auth system\n * Consumers should extend this with their own user properties\n */\nexport interface BaseUser {\n /** Unique user identifier */\n id: string;\n /** User email address */\n email: string;\n}\n\n/**\n * User with password hash - used internally for authentication\n */\nexport interface UserWithPassword extends BaseUser {\n /** Bcrypt hashed password */\n passwordHash: string;\n}\n\n/**\n * User with refresh token - used for token refresh operations\n */\nexport interface UserWithRefreshToken extends BaseUser {\n /** Hashed refresh token (nullable when logged out) */\n hashedRefreshToken: string | null;\n}\n\n/**\n * Full auth user interface combining all auth-related fields\n */\nexport interface AuthUser extends BaseUser {\n passwordHash: string;\n hashedRefreshToken: string | null;\n}\n\n/**\n * Login credentials\n */\nexport interface LoginCredentials {\n email: string;\n password: string;\n}\n\n/**\n * Registration data\n */\nexport interface RegisterData {\n email: string;\n password: string;\n}\n\n/**\n * Auth state for frontend applications\n */\nexport interface AuthState<TUser extends BaseUser = BaseUser> {\n /** Current authenticated user or null */\n user: TUser | null;\n /** Current tokens or null */\n tokens: AuthTokens | null;\n /** Whether auth state is being loaded/validated */\n isLoading: boolean;\n /** Whether user is authenticated */\n isAuthenticated: boolean;\n /** Auth error if any */\n error: string | null;\n}\n\n/**\n * Token storage interface for frontend applications\n * Implement this to use different storage backends (localStorage, SecureStore, etc.)\n */\nexport interface TokenStorage {\n getItem(key: string): Promise<string | null> | string | null;\n setItem(key: string, value: string): Promise<void> | void;\n removeItem(key: string): Promise<void> | void;\n}\n\n/**\n * Auth client configuration for frontend HTTP client\n */\nexport interface AuthClientConfig {\n /** Base URL for auth API endpoints */\n baseUrl: string;\n /** Token storage implementation */\n tokenStorage: TokenStorage;\n /** Custom headers to include in requests */\n headers?: Record<string, string>;\n /** Access token storage key (default: 'lenan_access_token') */\n accessTokenKey?: string;\n /** Refresh token storage key (default: 'lenan_refresh_token') */\n refreshTokenKey?: string;\n}\n\n/**\n * Auth API endpoints configuration\n */\nexport interface AuthEndpoints {\n login: string;\n register: string;\n refresh: string;\n logout: string;\n me: string;\n}\n\n/**\n * Default auth endpoints\n */\nexport const DEFAULT_AUTH_ENDPOINTS: AuthEndpoints = {\n login: \"/auth/login\",\n register: \"/auth/register\",\n refresh: \"/auth/refresh\",\n logout: \"/auth/logout\",\n me: \"/auth/me\",\n};\n\n/**\n * Default token storage keys\n */\nexport const TOKEN_STORAGE_KEYS = {\n ACCESS_TOKEN: \"lenan_access_token\",\n REFRESH_TOKEN: \"lenan_refresh_token\",\n} as const;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACmIO,IAAM,yBAAwC;AAAA,EACnD,OAAO;AAAA,EACP,UAAU;AAAA,EACV,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,IAAI;AACN;AAKO,IAAM,qBAAqB;AAAA,EAChC,cAAc;AAAA,EACd,eAAe;AACjB;","names":[]}
|