@spfn/auth 0.2.0-beta.3 → 0.2.0-beta.30
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 +689 -180
- package/dist/{dto-CLYtuAom.d.ts → authenticate-CT2Xy98P.d.ts} +390 -150
- package/dist/config.d.ts +100 -44
- package/dist/config.js +64 -35
- package/dist/config.js.map +1 -1
- package/dist/errors.d.ts +16 -2
- package/dist/errors.js +12 -0
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +256 -103
- package/dist/index.js +45 -1
- package/dist/index.js.map +1 -1
- package/dist/nextjs/api.js +202 -1
- package/dist/nextjs/api.js.map +1 -1
- package/dist/nextjs/client.d.ts +28 -0
- package/dist/nextjs/client.js +80 -0
- package/dist/nextjs/client.js.map +1 -0
- package/dist/nextjs/server.d.ts +89 -2
- package/dist/nextjs/server.js +147 -22
- package/dist/nextjs/server.js.map +1 -1
- package/dist/server.d.ts +620 -404
- package/dist/server.js +1087 -484
- package/dist/server.js.map +1 -1
- package/migrations/0001_smooth_the_fury.sql +3 -0
- package/migrations/meta/0001_snapshot.json +1660 -0
- package/migrations/meta/_journal.json +7 -0
- package/package.json +14 -10
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @spfn/auth - Technical Documentation
|
|
2
2
|
|
|
3
|
-
**Version:** 0.
|
|
3
|
+
**Version:** 0.2.0-beta.15
|
|
4
4
|
**Status:** Alpha - Internal Development
|
|
5
5
|
|
|
6
6
|
> **Note:** This is a technical documentation for developers working on the @spfn/auth package.
|
|
@@ -12,12 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
- [Overview](#overview)
|
|
14
14
|
- [Installation](#installation)
|
|
15
|
+
- [Admin Account Setup](#6-admin-account-setup)
|
|
15
16
|
- [Architecture](#architecture)
|
|
16
17
|
- [Package Structure](#package-structure)
|
|
17
18
|
- [Module Exports](#module-exports)
|
|
18
19
|
- [Email & SMS Services](#email--sms-services)
|
|
19
|
-
- [Email Templates](#email-templates)
|
|
20
20
|
- [Server-Side API](#server-side-api)
|
|
21
|
+
- [Events](#events)
|
|
22
|
+
- [OAuth Authentication](#oauth-authentication)
|
|
21
23
|
- [Database Schema](#database-schema)
|
|
22
24
|
- [RBAC System](#rbac-system)
|
|
23
25
|
- [Next.js Adapter](#nextjs-adapter)
|
|
@@ -34,10 +36,11 @@
|
|
|
34
36
|
|
|
35
37
|
- **Asymmetric JWT Authentication** - Client-signed tokens using ES256/RS256
|
|
36
38
|
- **User Management** - Email/phone-based identity with bcrypt hashing
|
|
39
|
+
- **OAuth Authentication** - Google OAuth 2.0 (Authorization Code Flow), extensible to other providers
|
|
37
40
|
- **Multi-Factor Authentication** - OTP verification via email/SMS
|
|
38
41
|
- **Session Management** - Public key rotation with 90-day expiry
|
|
39
42
|
- **Role-Based Access Control** - Flexible RBAC with runtime role/permission management
|
|
40
|
-
- **Next.js Integration** - Session helpers
|
|
43
|
+
- **Next.js Integration** - Session helpers, server-side guards, and OAuth interceptors
|
|
41
44
|
|
|
42
45
|
### Design Principles
|
|
43
46
|
|
|
@@ -90,18 +93,26 @@ export const appRouter = defineRouter({
|
|
|
90
93
|
|
|
91
94
|
### 3. Configure Client (Next.js)
|
|
92
95
|
|
|
93
|
-
####
|
|
96
|
+
#### Option A: Use the built-in `authApi` (Recommended)
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
import { authApi } from '@spfn/auth';
|
|
100
|
+
|
|
101
|
+
// Type-safe API calls for auth routes
|
|
102
|
+
const session = await authApi.getAuthSession.call({});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### Option B: Register Error Registry in Custom API Client
|
|
94
106
|
|
|
95
107
|
```typescript
|
|
96
108
|
import { createApi } from '@spfn/core/nextjs';
|
|
97
109
|
import type { AppRouter } from '@/server/router';
|
|
98
|
-
import { appMetadata as authAppMetadata } from "@spfn/auth";
|
|
99
110
|
import { authErrorRegistry } from "@spfn/auth/errors";
|
|
100
111
|
import { appMetadata } from '@/server/router.metadata';
|
|
101
112
|
import { errorRegistry } from "@spfn/core/errors";
|
|
102
113
|
|
|
103
114
|
export const api = createApi<AppRouter>({
|
|
104
|
-
metadata:
|
|
115
|
+
metadata: appMetadata,
|
|
105
116
|
errorRegistry: errorRegistry.concat(authErrorRegistry),
|
|
106
117
|
});
|
|
107
118
|
```
|
|
@@ -122,16 +133,19 @@ SPFN_AUTH_JWT_EXPIRES_IN=7d
|
|
|
122
133
|
SPFN_AUTH_BCRYPT_SALT_ROUNDS=10
|
|
123
134
|
SPFN_AUTH_SESSION_TTL=7d
|
|
124
135
|
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
136
|
+
# Google OAuth
|
|
137
|
+
SPFN_AUTH_GOOGLE_CLIENT_ID=123456789-abc.apps.googleusercontent.com
|
|
138
|
+
SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-...
|
|
139
|
+
SPFN_APP_URL=http://localhost:3000
|
|
140
|
+
|
|
141
|
+
# Google OAuth (Optional)
|
|
142
|
+
SPFN_AUTH_GOOGLE_SCOPES=email,profile,https://www.googleapis.com/auth/gmail.readonly
|
|
143
|
+
SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback
|
|
144
|
+
SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback
|
|
145
|
+
SPFN_AUTH_OAUTH_ERROR_URL=http://localhost:3000/auth/error?error={error}
|
|
130
146
|
|
|
131
|
-
#
|
|
132
|
-
|
|
133
|
-
SPFN_AUTH_AWS_SNS_SECRET_ACCESS_KEY=...
|
|
134
|
-
SPFN_AUTH_AWS_SNS_SENDER_ID=MyApp
|
|
147
|
+
# Email/SMS — configure via @spfn/notification
|
|
148
|
+
# See @spfn/notification README for AWS SES/SNS settings
|
|
135
149
|
```
|
|
136
150
|
|
|
137
151
|
### 5. Run Migrations
|
|
@@ -144,6 +158,83 @@ pnpm spfn db generate
|
|
|
144
158
|
pnpm spfn db migrate
|
|
145
159
|
```
|
|
146
160
|
|
|
161
|
+
### 6. Admin Account Setup
|
|
162
|
+
|
|
163
|
+
Admin accounts are automatically created on server startup via `createAuthLifecycle()`.
|
|
164
|
+
Choose one of the following methods:
|
|
165
|
+
|
|
166
|
+
#### Method 1: JSON Format (Recommended)
|
|
167
|
+
|
|
168
|
+
Best for multiple accounts with full configuration:
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
SPFN_AUTH_ADMIN_ACCOUNTS='[
|
|
172
|
+
{"email": "superadmin@example.com", "password": "secure-pass-1", "role": "superadmin"},
|
|
173
|
+
{"email": "admin@example.com", "password": "secure-pass-2", "role": "admin"},
|
|
174
|
+
{"email": "manager@example.com", "password": "secure-pass-3", "role": "user"}
|
|
175
|
+
]'
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**JSON Schema:**
|
|
179
|
+
```typescript
|
|
180
|
+
interface AdminAccountConfig {
|
|
181
|
+
email: string; // Required
|
|
182
|
+
password: string; // Required
|
|
183
|
+
role?: string; // Default: 'user' (options: 'user', 'admin', 'superadmin')
|
|
184
|
+
phone?: string; // Optional
|
|
185
|
+
passwordChangeRequired?: boolean; // Default: true
|
|
186
|
+
}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
#### Method 2: CSV Format
|
|
190
|
+
|
|
191
|
+
For multiple accounts with simpler configuration:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
SPFN_AUTH_ADMIN_EMAILS=admin@example.com,manager@example.com
|
|
195
|
+
SPFN_AUTH_ADMIN_PASSWORDS=admin-pass,manager-pass
|
|
196
|
+
SPFN_AUTH_ADMIN_ROLES=superadmin,admin
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### Method 3: Single Account (Legacy)
|
|
200
|
+
|
|
201
|
+
Simplest format for a single superadmin:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
SPFN_AUTH_ADMIN_EMAIL=admin@example.com
|
|
205
|
+
SPFN_AUTH_ADMIN_PASSWORD=secure-password
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
> **Note:** This method always creates a `superadmin` role account.
|
|
209
|
+
|
|
210
|
+
#### Default Behavior
|
|
211
|
+
|
|
212
|
+
All admin accounts created via environment variables have:
|
|
213
|
+
- `emailVerifiedAt`: Auto-verified (current timestamp)
|
|
214
|
+
- `passwordChangeRequired`: `true` (must change on first login)
|
|
215
|
+
- `status`: `active`
|
|
216
|
+
|
|
217
|
+
#### Programmatic Creation
|
|
218
|
+
|
|
219
|
+
You can also create admin accounts programmatically:
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
import { usersRepository, getRoleByName, hashPassword } from '@spfn/auth/server';
|
|
223
|
+
|
|
224
|
+
// After initializeAuth() has been called
|
|
225
|
+
const role = await getRoleByName('admin');
|
|
226
|
+
const passwordHash = await hashPassword('secure-password');
|
|
227
|
+
|
|
228
|
+
await usersRepository.create({
|
|
229
|
+
email: 'admin@example.com',
|
|
230
|
+
passwordHash,
|
|
231
|
+
roleId: role.id,
|
|
232
|
+
emailVerifiedAt: new Date(),
|
|
233
|
+
passwordChangeRequired: true,
|
|
234
|
+
status: 'active',
|
|
235
|
+
});
|
|
236
|
+
```
|
|
237
|
+
|
|
147
238
|
---
|
|
148
239
|
|
|
149
240
|
## Architecture
|
|
@@ -335,20 +426,15 @@ packages/auth/
|
|
|
335
426
|
|
|
336
427
|
### Common Module (`@spfn/auth`)
|
|
337
428
|
|
|
338
|
-
**
|
|
429
|
+
**API Client:**
|
|
339
430
|
```typescript
|
|
340
|
-
import {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
userPermissions,
|
|
348
|
-
userInvitations,
|
|
349
|
-
userSocialAccounts,
|
|
350
|
-
userProfiles
|
|
351
|
-
} from '@spfn/auth';
|
|
431
|
+
import { authApi } from '@spfn/auth';
|
|
432
|
+
|
|
433
|
+
// Type-safe API calls
|
|
434
|
+
const session = await authApi.getAuthSession.call({});
|
|
435
|
+
const result = await authApi.login.call({
|
|
436
|
+
body: { email, password, fingerprint, publicKey, keyId }
|
|
437
|
+
});
|
|
352
438
|
```
|
|
353
439
|
|
|
354
440
|
**Types:**
|
|
@@ -359,6 +445,9 @@ import type {
|
|
|
359
445
|
VerificationCode,
|
|
360
446
|
Role,
|
|
361
447
|
Permission,
|
|
448
|
+
AuthSession,
|
|
449
|
+
UserProfile,
|
|
450
|
+
ProfileInfo,
|
|
362
451
|
// ... etc
|
|
363
452
|
} from '@spfn/auth';
|
|
364
453
|
```
|
|
@@ -380,6 +469,34 @@ import type {
|
|
|
380
469
|
} from '@spfn/auth';
|
|
381
470
|
```
|
|
382
471
|
|
|
472
|
+
**Validation Patterns:**
|
|
473
|
+
```typescript
|
|
474
|
+
import {
|
|
475
|
+
UUID_PATTERN,
|
|
476
|
+
EMAIL_PATTERN,
|
|
477
|
+
BASE64_PATTERN,
|
|
478
|
+
FINGERPRINT_PATTERN,
|
|
479
|
+
PHONE_PATTERN,
|
|
480
|
+
} from '@spfn/auth';
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Route Map (for RPC Proxy):**
|
|
484
|
+
```typescript
|
|
485
|
+
import { authRouteMap } from '@spfn/auth';
|
|
486
|
+
|
|
487
|
+
// Use in Next.js RPC proxy (app/api/rpc/[routeName]/route.ts)
|
|
488
|
+
import '@spfn/auth/nextjs/api'; // Auto-register auth interceptors
|
|
489
|
+
import { routeMap } from '@/generated/route-map';
|
|
490
|
+
import { authRouteMap } from '@spfn/auth';
|
|
491
|
+
import { createRpcProxy } from '@spfn/core/nextjs/proxy';
|
|
492
|
+
|
|
493
|
+
export const { GET, POST } = createRpcProxy({
|
|
494
|
+
routeMap: { ...routeMap, ...authRouteMap }
|
|
495
|
+
});
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
> **Note:** Database entities (`users`, `userPublicKeys`, etc.) are exported from `@spfn/auth/server`, not the common module.
|
|
499
|
+
|
|
383
500
|
---
|
|
384
501
|
|
|
385
502
|
### Server Module (`@spfn/auth/server`)
|
|
@@ -456,22 +573,13 @@ import {
|
|
|
456
573
|
|
|
457
574
|
// Session
|
|
458
575
|
getAuthSessionService,
|
|
459
|
-
getUserProfileService,
|
|
460
576
|
|
|
461
|
-
//
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
// SMS
|
|
466
|
-
sendSMS,
|
|
467
|
-
registerSMSProvider,
|
|
577
|
+
// User Profile
|
|
578
|
+
getUserProfileService,
|
|
579
|
+
updateUserProfileService,
|
|
468
580
|
|
|
469
|
-
//
|
|
470
|
-
|
|
471
|
-
getVerificationCodeTemplate,
|
|
472
|
-
getWelcomeTemplate,
|
|
473
|
-
getPasswordResetTemplate,
|
|
474
|
-
getInvitationTemplate,
|
|
581
|
+
// OAuth - Google API Access
|
|
582
|
+
getGoogleAccessToken,
|
|
475
583
|
} from '@spfn/auth/server';
|
|
476
584
|
```
|
|
477
585
|
|
|
@@ -494,11 +602,13 @@ import {
|
|
|
494
602
|
```typescript
|
|
495
603
|
import {
|
|
496
604
|
authenticate,
|
|
605
|
+
optionalAuth,
|
|
497
606
|
requirePermissions,
|
|
607
|
+
requireAnyPermission,
|
|
498
608
|
requireRole,
|
|
499
609
|
} from '@spfn/auth/server';
|
|
500
610
|
|
|
501
|
-
// Usage
|
|
611
|
+
// Usage - all permissions required
|
|
502
612
|
app.bind(
|
|
503
613
|
myContract,
|
|
504
614
|
[authenticate, requirePermissions('user:delete')],
|
|
@@ -506,6 +616,27 @@ app.bind(
|
|
|
506
616
|
// Handler
|
|
507
617
|
}
|
|
508
618
|
);
|
|
619
|
+
|
|
620
|
+
// Usage - any of the permissions
|
|
621
|
+
app.bind(
|
|
622
|
+
myContract,
|
|
623
|
+
[authenticate, requireAnyPermission('content:read', 'admin:access')],
|
|
624
|
+
async (c) => {
|
|
625
|
+
// User has either content:read OR admin:access
|
|
626
|
+
}
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
// Usage - optional auth (public route with optional user context)
|
|
630
|
+
// Auto-skips global 'auth' middleware — no .skip(['auth']) needed
|
|
631
|
+
export const getProducts = route.get('/products')
|
|
632
|
+
.use([optionalAuth])
|
|
633
|
+
.handler(async (c) => {
|
|
634
|
+
const auth = getOptionalAuth(c); // AuthContext | undefined
|
|
635
|
+
if (auth) {
|
|
636
|
+
return getPersonalizedProducts(auth.userId);
|
|
637
|
+
}
|
|
638
|
+
return getPublicProducts();
|
|
639
|
+
});
|
|
509
640
|
```
|
|
510
641
|
|
|
511
642
|
**Helpers:**
|
|
@@ -513,6 +644,7 @@ app.bind(
|
|
|
513
644
|
import {
|
|
514
645
|
// Context
|
|
515
646
|
getAuth,
|
|
647
|
+
getOptionalAuth,
|
|
516
648
|
getUser,
|
|
517
649
|
getUserId,
|
|
518
650
|
getKeyId,
|
|
@@ -610,9 +742,11 @@ import {
|
|
|
610
742
|
loginRegisterInterceptor,
|
|
611
743
|
generalAuthInterceptor,
|
|
612
744
|
keyRotationInterceptor,
|
|
745
|
+
oauthUrlInterceptor,
|
|
746
|
+
oauthFinalizeInterceptor,
|
|
613
747
|
} from '@spfn/auth/nextjs/api';
|
|
614
748
|
|
|
615
|
-
// Auto-registers interceptors on import
|
|
749
|
+
// Auto-registers interceptors on import (including OAuth)
|
|
616
750
|
import '@spfn/auth/nextjs/api';
|
|
617
751
|
```
|
|
618
752
|
|
|
@@ -679,141 +813,24 @@ export default async function DashboardPage()
|
|
|
679
813
|
|
|
680
814
|
## Email & SMS Services
|
|
681
815
|
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
The email service uses AWS SES by default, with fallback to console logging in development.
|
|
685
|
-
|
|
686
|
-
**Send Email:**
|
|
687
|
-
```typescript
|
|
688
|
-
import { sendEmail } from '@spfn/auth/server';
|
|
689
|
-
|
|
690
|
-
await sendEmail({
|
|
691
|
-
to: 'user@example.com',
|
|
692
|
-
subject: 'Welcome!',
|
|
693
|
-
text: 'Plain text content',
|
|
694
|
-
html: '<h1>HTML content</h1>',
|
|
695
|
-
purpose: 'welcome', // for logging
|
|
696
|
-
});
|
|
697
|
-
```
|
|
698
|
-
|
|
699
|
-
**Custom Email Provider:**
|
|
700
|
-
```typescript
|
|
701
|
-
import { registerEmailProvider } from '@spfn/auth/server';
|
|
702
|
-
|
|
703
|
-
// Register SendGrid provider
|
|
704
|
-
registerEmailProvider({
|
|
705
|
-
name: 'sendgrid',
|
|
706
|
-
sendEmail: async ({ to, subject, text, html }) => {
|
|
707
|
-
// Your SendGrid implementation
|
|
708
|
-
return { success: true, messageId: '...' };
|
|
709
|
-
},
|
|
710
|
-
});
|
|
711
|
-
```
|
|
712
|
-
|
|
713
|
-
---
|
|
714
|
-
|
|
715
|
-
### SMS Service
|
|
716
|
-
|
|
717
|
-
The SMS service uses AWS SNS by default.
|
|
718
|
-
|
|
719
|
-
**Send SMS:**
|
|
720
|
-
```typescript
|
|
721
|
-
import { sendSMS } from '@spfn/auth/server';
|
|
722
|
-
|
|
723
|
-
await sendSMS({
|
|
724
|
-
phone: '+821012345678', // E.164 format
|
|
725
|
-
message: 'Your code is: 123456',
|
|
726
|
-
purpose: 'verification',
|
|
727
|
-
});
|
|
728
|
-
```
|
|
729
|
-
|
|
730
|
-
**Custom SMS Provider:**
|
|
731
|
-
```typescript
|
|
732
|
-
import { registerSMSProvider } from '@spfn/auth/server';
|
|
733
|
-
|
|
734
|
-
// Register Twilio provider
|
|
735
|
-
registerSMSProvider({
|
|
736
|
-
name: 'twilio',
|
|
737
|
-
sendSMS: async ({ phone, message }) => {
|
|
738
|
-
// Your Twilio implementation
|
|
739
|
-
return { success: true, messageId: '...' };
|
|
740
|
-
},
|
|
741
|
-
});
|
|
742
|
-
```
|
|
743
|
-
|
|
744
|
-
---
|
|
745
|
-
|
|
746
|
-
## Email Templates
|
|
747
|
-
|
|
748
|
-
### Built-in Templates
|
|
749
|
-
|
|
750
|
-
| Template | Function | Purpose |
|
|
751
|
-
|----------|----------|---------|
|
|
752
|
-
| `verificationCode` | `getVerificationCodeTemplate` | Verification codes (registration, login, password reset) |
|
|
753
|
-
| `welcome` | `getWelcomeTemplate` | Welcome email after registration |
|
|
754
|
-
| `passwordReset` | `getPasswordResetTemplate` | Password reset link |
|
|
755
|
-
| `invitation` | `getInvitationTemplate` | User invitation |
|
|
756
|
-
|
|
757
|
-
**Usage:**
|
|
758
|
-
```typescript
|
|
759
|
-
import { getVerificationCodeTemplate, sendEmail } from '@spfn/auth/server';
|
|
760
|
-
|
|
761
|
-
const { subject, text, html } = getVerificationCodeTemplate({
|
|
762
|
-
code: '123456',
|
|
763
|
-
purpose: 'registration',
|
|
764
|
-
expiresInMinutes: 5,
|
|
765
|
-
appName: 'MyApp',
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
await sendEmail({ to: 'user@example.com', subject, text, html });
|
|
769
|
-
```
|
|
770
|
-
|
|
771
|
-
---
|
|
816
|
+
> **⚠️ DEPRECATED:** Email and SMS functionality has been moved to `@spfn/notification` package.
|
|
772
817
|
|
|
773
|
-
###
|
|
774
|
-
|
|
775
|
-
Register custom templates to override defaults with your brand design:
|
|
818
|
+
### Migration Guide
|
|
776
819
|
|
|
777
820
|
```typescript
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
// Register at app initialization (e.g., server.config.ts)
|
|
781
|
-
registerEmailTemplates({
|
|
782
|
-
// Override verification code template
|
|
783
|
-
verificationCode: ({ code, purpose, expiresInMinutes, appName }) => ({
|
|
784
|
-
subject: `[${appName}] Your verification code`,
|
|
785
|
-
text: `Your code: ${code}\nExpires in ${expiresInMinutes} minutes.`,
|
|
786
|
-
html: `
|
|
787
|
-
<div style="font-family: Arial, sans-serif;">
|
|
788
|
-
<img src="https://myapp.com/logo.png" alt="Logo" />
|
|
789
|
-
<h1>Verification Code</h1>
|
|
790
|
-
<div style="font-size: 32px; font-weight: bold;">${code}</div>
|
|
791
|
-
<p>This code expires in ${expiresInMinutes} minutes.</p>
|
|
792
|
-
</div>
|
|
793
|
-
`,
|
|
794
|
-
}),
|
|
821
|
+
// Before (deprecated)
|
|
822
|
+
import { sendEmail, sendSMS } from '@spfn/auth/server';
|
|
795
823
|
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
subject: `${inviterName} invited you to ${appName}`,
|
|
799
|
-
text: `Accept invitation: ${inviteLink}`,
|
|
800
|
-
html: `
|
|
801
|
-
<h1>You're Invited!</h1>
|
|
802
|
-
<p>${inviterName} invited you to join ${appName} as ${roleName}.</p>
|
|
803
|
-
<a href="${inviteLink}">Accept Invitation</a>
|
|
804
|
-
`,
|
|
805
|
-
}),
|
|
806
|
-
});
|
|
824
|
+
// After (recommended)
|
|
825
|
+
import { sendEmail, sendSMS } from '@spfn/notification/server';
|
|
807
826
|
```
|
|
808
827
|
|
|
809
|
-
|
|
828
|
+
The `@spfn/notification` package provides:
|
|
829
|
+
- Multi-channel support (Email, SMS, Slack, Push)
|
|
830
|
+
- Template system with variable substitution
|
|
831
|
+
- Multiple provider support (AWS SES, SNS, SendGrid, Twilio, etc.)
|
|
810
832
|
|
|
811
|
-
|
|
812
|
-
|----------|------------|
|
|
813
|
-
| `verificationCode` | `code`, `purpose`, `expiresInMinutes?`, `appName?` |
|
|
814
|
-
| `welcome` | `email`, `appName?` |
|
|
815
|
-
| `passwordReset` | `resetLink`, `expiresInMinutes?`, `appName?` |
|
|
816
|
-
| `invitation` | `inviteLink`, `inviterName?`, `roleName?`, `appName?` |
|
|
833
|
+
For documentation, see `@spfn/notification` package README.
|
|
817
834
|
|
|
818
835
|
---
|
|
819
836
|
|
|
@@ -1027,6 +1044,475 @@ Change password.
|
|
|
1027
1044
|
|
|
1028
1045
|
---
|
|
1029
1046
|
|
|
1047
|
+
#### `GET /_auth/users/username/check`
|
|
1048
|
+
|
|
1049
|
+
Check if a username is available.
|
|
1050
|
+
|
|
1051
|
+
**Query:**
|
|
1052
|
+
```typescript
|
|
1053
|
+
{
|
|
1054
|
+
username: string; // Min 1 char
|
|
1055
|
+
}
|
|
1056
|
+
```
|
|
1057
|
+
|
|
1058
|
+
**Response:**
|
|
1059
|
+
```typescript
|
|
1060
|
+
{
|
|
1061
|
+
available: boolean;
|
|
1062
|
+
}
|
|
1063
|
+
```
|
|
1064
|
+
|
|
1065
|
+
---
|
|
1066
|
+
|
|
1067
|
+
#### `PATCH /_auth/users/username`
|
|
1068
|
+
|
|
1069
|
+
Update authenticated user's username. Validates uniqueness before updating.
|
|
1070
|
+
|
|
1071
|
+
**Request:**
|
|
1072
|
+
```typescript
|
|
1073
|
+
{
|
|
1074
|
+
username: string | null; // New username or null to clear
|
|
1075
|
+
}
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
**Response:** Updated user object.
|
|
1079
|
+
|
|
1080
|
+
**Errors:**
|
|
1081
|
+
- `409 UsernameAlreadyTakenError` - Username is already in use by another user
|
|
1082
|
+
|
|
1083
|
+
---
|
|
1084
|
+
|
|
1085
|
+
## Events
|
|
1086
|
+
|
|
1087
|
+
`@spfn/auth`는 `@spfn/core/event`를 사용하여 인증 관련 이벤트를 발행합니다. 이를 통해 로그인/회원가입 시 추가 로직(환영 이메일, 분석, 알림 등)을 디커플링된 방식으로 처리할 수 있습니다.
|
|
1088
|
+
|
|
1089
|
+
### Available Events
|
|
1090
|
+
|
|
1091
|
+
| Event | Description | Trigger |
|
|
1092
|
+
|-------|-------------|---------|
|
|
1093
|
+
| `auth.login` | 로그인 성공 | 이메일/전화 로그인, OAuth 기존 사용자 |
|
|
1094
|
+
| `auth.register` | 회원가입 성공 | 이메일/전화 회원가입, OAuth 신규 사용자 |
|
|
1095
|
+
|
|
1096
|
+
---
|
|
1097
|
+
|
|
1098
|
+
### Event Payloads
|
|
1099
|
+
|
|
1100
|
+
#### `auth.login`
|
|
1101
|
+
|
|
1102
|
+
```typescript
|
|
1103
|
+
{
|
|
1104
|
+
userId: string;
|
|
1105
|
+
provider: 'email' | 'phone' | 'google';
|
|
1106
|
+
email?: string;
|
|
1107
|
+
phone?: string;
|
|
1108
|
+
}
|
|
1109
|
+
```
|
|
1110
|
+
|
|
1111
|
+
#### `auth.register`
|
|
1112
|
+
|
|
1113
|
+
```typescript
|
|
1114
|
+
{
|
|
1115
|
+
userId: string;
|
|
1116
|
+
provider: 'email' | 'phone' | 'google';
|
|
1117
|
+
email?: string;
|
|
1118
|
+
phone?: string;
|
|
1119
|
+
}
|
|
1120
|
+
```
|
|
1121
|
+
|
|
1122
|
+
---
|
|
1123
|
+
|
|
1124
|
+
### Subscribing to Events
|
|
1125
|
+
|
|
1126
|
+
```typescript
|
|
1127
|
+
import { authLoginEvent, authRegisterEvent } from '@spfn/auth/server';
|
|
1128
|
+
|
|
1129
|
+
// 로그인 이벤트 구독
|
|
1130
|
+
authLoginEvent.subscribe(async (payload) => {
|
|
1131
|
+
console.log('User logged in:', payload.userId, payload.provider);
|
|
1132
|
+
await analytics.trackLogin(payload.userId);
|
|
1133
|
+
});
|
|
1134
|
+
|
|
1135
|
+
// 회원가입 이벤트 구독
|
|
1136
|
+
authRegisterEvent.subscribe(async (payload) => {
|
|
1137
|
+
console.log('New user registered:', payload.userId);
|
|
1138
|
+
if (payload.email) {
|
|
1139
|
+
await emailService.sendWelcome(payload.email);
|
|
1140
|
+
}
|
|
1141
|
+
});
|
|
1142
|
+
```
|
|
1143
|
+
|
|
1144
|
+
---
|
|
1145
|
+
|
|
1146
|
+
### Job Integration
|
|
1147
|
+
|
|
1148
|
+
`@spfn/core/job`과 연동하여 백그라운드 작업을 실행할 수 있습니다.
|
|
1149
|
+
|
|
1150
|
+
```typescript
|
|
1151
|
+
import { job, defineJobRouter } from '@spfn/core/job';
|
|
1152
|
+
import { authRegisterEvent } from '@spfn/auth/server';
|
|
1153
|
+
|
|
1154
|
+
// 회원가입 시 환영 이메일 발송 Job
|
|
1155
|
+
const sendWelcomeEmailJob = job('send-welcome-email')
|
|
1156
|
+
.on(authRegisterEvent)
|
|
1157
|
+
.handler(async ({ userId, email }) => {
|
|
1158
|
+
if (email) {
|
|
1159
|
+
await emailService.sendWelcome(email);
|
|
1160
|
+
}
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// 회원가입 시 기본 설정 생성 Job
|
|
1164
|
+
const createDefaultSettingsJob = job('create-default-settings')
|
|
1165
|
+
.on(authRegisterEvent)
|
|
1166
|
+
.handler(async ({ userId }) => {
|
|
1167
|
+
await settingsService.createDefaults(userId);
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
export const jobRouter = defineJobRouter({
|
|
1171
|
+
sendWelcomeEmailJob,
|
|
1172
|
+
createDefaultSettingsJob,
|
|
1173
|
+
});
|
|
1174
|
+
```
|
|
1175
|
+
|
|
1176
|
+
---
|
|
1177
|
+
|
|
1178
|
+
### Event Flow
|
|
1179
|
+
|
|
1180
|
+
```
|
|
1181
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
1182
|
+
│ loginService() / registerService() │
|
|
1183
|
+
│ oauthCallbackService() │
|
|
1184
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
1185
|
+
│
|
|
1186
|
+
▼
|
|
1187
|
+
authLoginEvent.emit()
|
|
1188
|
+
authRegisterEvent.emit()
|
|
1189
|
+
│
|
|
1190
|
+
┌───────────────────┼───────────────────┐
|
|
1191
|
+
▼ ▼ ▼
|
|
1192
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
1193
|
+
│ Backend │ │ Job │ │ SSE │
|
|
1194
|
+
│ Handler │ │ Queue │ │ Stream │
|
|
1195
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
1196
|
+
.subscribe() .on(event) (optional)
|
|
1197
|
+
│ │
|
|
1198
|
+
▼ ▼
|
|
1199
|
+
[Analytics, [Background
|
|
1200
|
+
Logging] Processing]
|
|
1201
|
+
```
|
|
1202
|
+
|
|
1203
|
+
---
|
|
1204
|
+
|
|
1205
|
+
### Type Exports
|
|
1206
|
+
|
|
1207
|
+
```typescript
|
|
1208
|
+
import type {
|
|
1209
|
+
AuthLoginPayload,
|
|
1210
|
+
AuthRegisterPayload,
|
|
1211
|
+
} from '@spfn/auth/server';
|
|
1212
|
+
```
|
|
1213
|
+
|
|
1214
|
+
---
|
|
1215
|
+
|
|
1216
|
+
## OAuth Authentication
|
|
1217
|
+
|
|
1218
|
+
### Overview
|
|
1219
|
+
|
|
1220
|
+
`@spfn/auth`는 OAuth 2.0 Authorization Code Flow를 지원합니다. 현재 Google OAuth가 구현되어 있으며, 다른 provider (GitHub, Kakao, Naver)는 동일한 패턴으로 확장 가능합니다.
|
|
1221
|
+
|
|
1222
|
+
**핵심 설계:**
|
|
1223
|
+
- 환경 변수만으로 설정 (`SPFN_AUTH_GOOGLE_CLIENT_ID`, `SPFN_AUTH_GOOGLE_CLIENT_SECRET`)
|
|
1224
|
+
- Next.js 인터셉터 기반 자동 세션 관리 (키쌍 생성 → pending session → full session)
|
|
1225
|
+
- 기존 이메일 계정과 자동 연결 (Google verified_email 확인 시에만)
|
|
1226
|
+
|
|
1227
|
+
---
|
|
1228
|
+
|
|
1229
|
+
### Authentication Flow
|
|
1230
|
+
|
|
1231
|
+
```
|
|
1232
|
+
┌──────────┐ ┌──────────────┐ ┌──────────┐ ┌──────────┐
|
|
1233
|
+
│ Client │ │ Next.js RPC │ │ Backend │ │ Google │
|
|
1234
|
+
│ (Browser)│ │ (Interceptor)│ │ (SPFN) │ │ OAuth │
|
|
1235
|
+
└────┬─────┘ └──────┬───────┘ └────┬─────┘ └────┬─────┘
|
|
1236
|
+
│ │ │ │
|
|
1237
|
+
│ 1. Click Login │ │ │
|
|
1238
|
+
├──────────────────>│ │ │
|
|
1239
|
+
│ │ │ │
|
|
1240
|
+
│ 2. Generate keypair (ES256) │ │
|
|
1241
|
+
│ 3. Create encrypted state │ │
|
|
1242
|
+
│ (publicKey, keyId in JWE) │ │
|
|
1243
|
+
│ 4. Save privateKey to │ │
|
|
1244
|
+
│ pending session cookie │ │
|
|
1245
|
+
│ │ │ │
|
|
1246
|
+
│ │ 5. Forward with │ │
|
|
1247
|
+
│ │ state in body │ │
|
|
1248
|
+
│ ├─────────────────>│ │
|
|
1249
|
+
│ │ │ │
|
|
1250
|
+
│ │ 6. Return Google │ │
|
|
1251
|
+
│ │ Auth URL │ │
|
|
1252
|
+
│ │<─────────────────┤ │
|
|
1253
|
+
│ │ │ │
|
|
1254
|
+
│ 7. Redirect to Google │ │
|
|
1255
|
+
│<──────────────────┤ │ │
|
|
1256
|
+
│ │ │ │
|
|
1257
|
+
│ 8. User consents │ │ │
|
|
1258
|
+
├───────────────────┼──────────────────┼────────────────>│
|
|
1259
|
+
│ │ │ │
|
|
1260
|
+
│ │ 9. Callback with code + state │
|
|
1261
|
+
│ │ │<────────────────┤
|
|
1262
|
+
│ │ │ │
|
|
1263
|
+
│ │ 10. Verify state, exchange code │
|
|
1264
|
+
│ │ Create/link user account │
|
|
1265
|
+
│ │ Register publicKey │
|
|
1266
|
+
│ │ │ │
|
|
1267
|
+
│ 11. Redirect to /auth/callback │ │
|
|
1268
|
+
│ ?userId=X&keyId=Y&returnUrl=/ │ │
|
|
1269
|
+
│<─────────────────────────────────────┤ │
|
|
1270
|
+
│ │ │ │
|
|
1271
|
+
│ 12. OAuthCallback │ │ │
|
|
1272
|
+
│ component │ │ │
|
|
1273
|
+
│ calls finalize│ │ │
|
|
1274
|
+
├──────────────────>│ │ │
|
|
1275
|
+
│ │ │ │
|
|
1276
|
+
│ 13. Interceptor reads pending │ │
|
|
1277
|
+
│ session cookie, verifies │ │
|
|
1278
|
+
│ keyId match, creates full │ │
|
|
1279
|
+
│ session cookie │ │
|
|
1280
|
+
│ │ │ │
|
|
1281
|
+
│ 14. Session set, │ │ │
|
|
1282
|
+
│ redirect to │ │ │
|
|
1283
|
+
│ returnUrl │ │ │
|
|
1284
|
+
│<──────────────────┤ │ │
|
|
1285
|
+
│ │ │ │
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
---
|
|
1289
|
+
|
|
1290
|
+
### Setup
|
|
1291
|
+
|
|
1292
|
+
#### 1. Google Cloud Console
|
|
1293
|
+
|
|
1294
|
+
1. [Google Cloud Console](https://console.cloud.google.com/) > APIs & Services > Credentials
|
|
1295
|
+
2. Create OAuth 2.0 Client ID (Web application)
|
|
1296
|
+
3. Add Authorized redirect URI: `http://localhost:8790/_auth/oauth/google/callback`
|
|
1297
|
+
4. Copy Client ID and Client Secret
|
|
1298
|
+
|
|
1299
|
+
#### 2. Environment Variables
|
|
1300
|
+
|
|
1301
|
+
```bash
|
|
1302
|
+
# Required
|
|
1303
|
+
SPFN_AUTH_GOOGLE_CLIENT_ID=your-client-id.apps.googleusercontent.com
|
|
1304
|
+
SPFN_AUTH_GOOGLE_CLIENT_SECRET=GOCSPX-your-secret
|
|
1305
|
+
|
|
1306
|
+
# Next.js app URL (for OAuth callback redirect)
|
|
1307
|
+
SPFN_APP_URL=http://localhost:3000
|
|
1308
|
+
|
|
1309
|
+
# Optional
|
|
1310
|
+
SPFN_AUTH_GOOGLE_SCOPES=email,profile # default (comma-separated)
|
|
1311
|
+
SPFN_AUTH_GOOGLE_REDIRECT_URI=http://localhost:8790/_auth/oauth/google/callback # default
|
|
1312
|
+
SPFN_AUTH_OAUTH_SUCCESS_URL=/auth/callback # default
|
|
1313
|
+
```
|
|
1314
|
+
|
|
1315
|
+
#### 3. Next.js Callback Page
|
|
1316
|
+
|
|
1317
|
+
```tsx
|
|
1318
|
+
// app/auth/callback/page.tsx
|
|
1319
|
+
export { OAuthCallback as default } from '@spfn/auth/nextjs/client';
|
|
1320
|
+
```
|
|
1321
|
+
|
|
1322
|
+
#### 4. Login Button
|
|
1323
|
+
|
|
1324
|
+
```typescript
|
|
1325
|
+
import { authApi } from '@spfn/auth';
|
|
1326
|
+
|
|
1327
|
+
const handleGoogleLogin = async () =>
|
|
1328
|
+
{
|
|
1329
|
+
const response = await authApi.getGoogleOAuthUrl.call({
|
|
1330
|
+
body: { returnUrl: '/dashboard' },
|
|
1331
|
+
});
|
|
1332
|
+
window.location.href = response.authUrl;
|
|
1333
|
+
};
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
---
|
|
1337
|
+
|
|
1338
|
+
### OAuth Routes
|
|
1339
|
+
|
|
1340
|
+
#### `GET /_auth/oauth/google`
|
|
1341
|
+
|
|
1342
|
+
Google OAuth 시작 (리다이렉트 방식). 브라우저를 Google 로그인 페이지로 직접 리다이렉트합니다.
|
|
1343
|
+
|
|
1344
|
+
**Query:**
|
|
1345
|
+
```typescript
|
|
1346
|
+
{
|
|
1347
|
+
state: string; // Encrypted OAuth state (JWE)
|
|
1348
|
+
}
|
|
1349
|
+
```
|
|
1350
|
+
|
|
1351
|
+
---
|
|
1352
|
+
|
|
1353
|
+
#### `POST /_auth/oauth/google/url`
|
|
1354
|
+
|
|
1355
|
+
Google OAuth URL 획득 (인터셉터 방식). 인터셉터가 state를 자동 생성하여 주입합니다.
|
|
1356
|
+
|
|
1357
|
+
**Request:**
|
|
1358
|
+
```typescript
|
|
1359
|
+
{
|
|
1360
|
+
returnUrl?: string; // Default: '/'
|
|
1361
|
+
}
|
|
1362
|
+
```
|
|
1363
|
+
|
|
1364
|
+
**Response:**
|
|
1365
|
+
```typescript
|
|
1366
|
+
{
|
|
1367
|
+
authUrl: string; // Google OAuth URL
|
|
1368
|
+
}
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
---
|
|
1372
|
+
|
|
1373
|
+
#### `GET /_auth/oauth/google/callback`
|
|
1374
|
+
|
|
1375
|
+
Google에서 리다이렉트되는 콜백. code를 token으로 교환하고 사용자를 생성/연결합니다.
|
|
1376
|
+
|
|
1377
|
+
**Query (from Google):**
|
|
1378
|
+
```typescript
|
|
1379
|
+
{
|
|
1380
|
+
code?: string; // Authorization code
|
|
1381
|
+
state?: string; // OAuth state
|
|
1382
|
+
error?: string; // Error code
|
|
1383
|
+
error_description?: string; // Error description
|
|
1384
|
+
}
|
|
1385
|
+
```
|
|
1386
|
+
|
|
1387
|
+
**Result:** Next.js 콜백 페이지로 리다이렉트 (`/auth/callback?userId=X&keyId=Y&returnUrl=/`)
|
|
1388
|
+
|
|
1389
|
+
---
|
|
1390
|
+
|
|
1391
|
+
#### `POST /_auth/oauth/finalize`
|
|
1392
|
+
|
|
1393
|
+
OAuth 세션 완료. 인터셉터가 pending session에서 full session을 생성합니다.
|
|
1394
|
+
|
|
1395
|
+
**Request:**
|
|
1396
|
+
```typescript
|
|
1397
|
+
{
|
|
1398
|
+
userId: string;
|
|
1399
|
+
keyId: string;
|
|
1400
|
+
returnUrl?: string;
|
|
1401
|
+
}
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
**Response:**
|
|
1405
|
+
```typescript
|
|
1406
|
+
{
|
|
1407
|
+
success: boolean;
|
|
1408
|
+
returnUrl: string;
|
|
1409
|
+
}
|
|
1410
|
+
```
|
|
1411
|
+
|
|
1412
|
+
---
|
|
1413
|
+
|
|
1414
|
+
#### `GET /_auth/oauth/providers`
|
|
1415
|
+
|
|
1416
|
+
활성화된 OAuth provider 목록을 반환합니다.
|
|
1417
|
+
|
|
1418
|
+
**Response:**
|
|
1419
|
+
```typescript
|
|
1420
|
+
{
|
|
1421
|
+
providers: ('google' | 'github' | 'kakao' | 'naver')[];
|
|
1422
|
+
}
|
|
1423
|
+
```
|
|
1424
|
+
|
|
1425
|
+
---
|
|
1426
|
+
|
|
1427
|
+
### Google API Access
|
|
1428
|
+
|
|
1429
|
+
OAuth 로그인 후 저장된 access token으로 Google API를 호출할 수 있습니다.
|
|
1430
|
+
|
|
1431
|
+
#### Custom Scopes 설정
|
|
1432
|
+
|
|
1433
|
+
`SPFN_AUTH_GOOGLE_SCOPES` 환경변수로 추가 스코프를 요청합니다. 미설정 시 `email,profile`이 기본값입니다.
|
|
1434
|
+
|
|
1435
|
+
```bash
|
|
1436
|
+
# Gmail + Calendar 읽기 권한 추가
|
|
1437
|
+
SPFN_AUTH_GOOGLE_SCOPES=email,profile,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/calendar.readonly
|
|
1438
|
+
```
|
|
1439
|
+
|
|
1440
|
+
> **Note:** Google Cloud Console에서 해당 API를 활성화해야 합니다.
|
|
1441
|
+
|
|
1442
|
+
#### Access Token 사용
|
|
1443
|
+
|
|
1444
|
+
`getGoogleAccessToken(userId)`은 유효한 access token을 반환합니다. 토큰이 만료 임박(5분 이내) 또는 만료 상태이면 자동으로 refresh token을 사용하여 갱신합니다.
|
|
1445
|
+
|
|
1446
|
+
```typescript
|
|
1447
|
+
import { getGoogleAccessToken } from '@spfn/auth/server';
|
|
1448
|
+
|
|
1449
|
+
// 항상 유효한 토큰 반환 (만료 시 자동 갱신)
|
|
1450
|
+
const token = await getGoogleAccessToken(userId);
|
|
1451
|
+
|
|
1452
|
+
// Gmail API 호출
|
|
1453
|
+
const response = await fetch(
|
|
1454
|
+
'https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10',
|
|
1455
|
+
{ headers: { Authorization: `Bearer ${token}` } }
|
|
1456
|
+
);
|
|
1457
|
+
const data = await response.json();
|
|
1458
|
+
```
|
|
1459
|
+
|
|
1460
|
+
**에러 케이스:**
|
|
1461
|
+
- Google 계정 미연결 → `'No Google account linked'`
|
|
1462
|
+
- Refresh token 없음 → `'Google refresh token not available'` (재로그인 필요)
|
|
1463
|
+
|
|
1464
|
+
---
|
|
1465
|
+
|
|
1466
|
+
### Security
|
|
1467
|
+
|
|
1468
|
+
- **State 암호화**: JWE (A256GCM)로 state 파라미터 암호화. CSRF 방지용 nonce 포함.
|
|
1469
|
+
- **Pending Session**: OAuth 리다이렉트 중 privateKey를 JWE로 암호화한 HttpOnly 쿠키에 저장. 10분 TTL.
|
|
1470
|
+
- **KeyId 검증**: finalize 시 pending session의 keyId와 응답의 keyId 일치 확인.
|
|
1471
|
+
- **Email 검증**: `verified_email`이 true인 경우에만 기존 계정에 자동 연결. 미검증 이메일로 기존 계정 연결 시도 시 에러.
|
|
1472
|
+
- **Session Cookie**: `HttpOnly`, `Secure` (production), `SameSite=strict`.
|
|
1473
|
+
|
|
1474
|
+
---
|
|
1475
|
+
|
|
1476
|
+
### OAuthCallback Component
|
|
1477
|
+
|
|
1478
|
+
`@spfn/auth/nextjs/client`에서 제공하는 클라이언트 컴포넌트입니다.
|
|
1479
|
+
|
|
1480
|
+
```tsx
|
|
1481
|
+
import { OAuthCallback } from '@spfn/auth/nextjs/client';
|
|
1482
|
+
|
|
1483
|
+
// 기본 사용
|
|
1484
|
+
export default function CallbackPage()
|
|
1485
|
+
{
|
|
1486
|
+
return <OAuthCallback />;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// 커스터마이징
|
|
1490
|
+
export default function CallbackPage()
|
|
1491
|
+
{
|
|
1492
|
+
return (
|
|
1493
|
+
<OAuthCallback
|
|
1494
|
+
apiBasePath="/api/rpc"
|
|
1495
|
+
loadingComponent={<MySpinner />}
|
|
1496
|
+
errorComponent={(error) => <MyError message={error} />}
|
|
1497
|
+
onSuccess={(userId) => console.log('Logged in:', userId)}
|
|
1498
|
+
onError={(error) => console.error(error)}
|
|
1499
|
+
/>
|
|
1500
|
+
);
|
|
1501
|
+
}
|
|
1502
|
+
```
|
|
1503
|
+
|
|
1504
|
+
**Props:**
|
|
1505
|
+
|
|
1506
|
+
| Prop | Type | Default | Description |
|
|
1507
|
+
|------|------|---------|-------------|
|
|
1508
|
+
| `apiBasePath` | `string` | `'/api/rpc'` | RPC API base path |
|
|
1509
|
+
| `loadingComponent` | `ReactNode` | Built-in | 로딩 중 표시할 컴포넌트 |
|
|
1510
|
+
| `errorComponent` | `(error: string) => ReactNode` | Built-in | 에러 표시 컴포넌트 |
|
|
1511
|
+
| `onSuccess` | `(userId: string) => void` | - | 성공 콜백 |
|
|
1512
|
+
| `onError` | `(error: string) => void` | - | 에러 콜백 |
|
|
1513
|
+
|
|
1514
|
+
---
|
|
1515
|
+
|
|
1030
1516
|
## Database Schema
|
|
1031
1517
|
|
|
1032
1518
|
### Core Tables
|
|
@@ -1040,6 +1526,7 @@ CREATE TABLE users (
|
|
|
1040
1526
|
id BIGSERIAL PRIMARY KEY,
|
|
1041
1527
|
email TEXT UNIQUE,
|
|
1042
1528
|
phone TEXT UNIQUE,
|
|
1529
|
+
username TEXT UNIQUE,
|
|
1043
1530
|
password_hash TEXT NOT NULL,
|
|
1044
1531
|
password_change_required BOOLEAN DEFAULT false,
|
|
1045
1532
|
role_id BIGINT REFERENCES roles(id) NOT NULL,
|
|
@@ -1058,6 +1545,7 @@ CREATE TABLE users (
|
|
|
1058
1545
|
|
|
1059
1546
|
**Key Points:**
|
|
1060
1547
|
- At least one of `email` OR `phone` required
|
|
1548
|
+
- `username` is unique and nullable (optional display/mention identifier)
|
|
1061
1549
|
- `passwordHash` is bcrypt ($2b$10$..., 60 chars)
|
|
1062
1550
|
- `roleId` references roles table (NOT NULL)
|
|
1063
1551
|
|
|
@@ -1167,7 +1655,7 @@ CREATE TABLE permissions (
|
|
|
1167
1655
|
|
|
1168
1656
|
**Built-in Permissions:**
|
|
1169
1657
|
- `auth:self:manage`
|
|
1170
|
-
- `user:read`, `user:write`, `user:delete`
|
|
1658
|
+
- `user:read`, `user:write`, `user:delete`, `user:invite`
|
|
1171
1659
|
- `rbac:role:manage`, `rbac:permission:manage`
|
|
1172
1660
|
|
|
1173
1661
|
---
|
|
@@ -1260,7 +1748,7 @@ CREATE TABLE user_profiles (
|
|
|
1260
1748
|
|
|
1261
1749
|
#### `user_social_accounts`
|
|
1262
1750
|
|
|
1263
|
-
OAuth provider accounts (
|
|
1751
|
+
OAuth provider accounts (Google, GitHub, etc.).
|
|
1264
1752
|
|
|
1265
1753
|
```sql
|
|
1266
1754
|
CREATE TABLE user_social_accounts (
|
|
@@ -1327,7 +1815,7 @@ await initializeAuth({
|
|
|
1327
1815
|
|
|
1328
1816
|
**Permissions:**
|
|
1329
1817
|
- `auth:self:manage` - Change password, rotate keys
|
|
1330
|
-
- `user:read`, `user:write`, `user:delete`
|
|
1818
|
+
- `user:read`, `user:write`, `user:delete`, `user:invite`
|
|
1331
1819
|
- `rbac:role:manage`, `rbac:permission:manage`
|
|
1332
1820
|
|
|
1333
1821
|
---
|
|
@@ -1335,7 +1823,7 @@ await initializeAuth({
|
|
|
1335
1823
|
### Middleware Usage
|
|
1336
1824
|
|
|
1337
1825
|
```typescript
|
|
1338
|
-
import { authenticate, requirePermissions, requireRole } from '@spfn/auth/server';
|
|
1826
|
+
import { authenticate, requirePermissions, requireAnyPermission, requireRole } from '@spfn/auth/server';
|
|
1339
1827
|
|
|
1340
1828
|
// Single permission
|
|
1341
1829
|
app.bind(
|
|
@@ -1355,6 +1843,15 @@ app.bind(
|
|
|
1355
1843
|
}
|
|
1356
1844
|
);
|
|
1357
1845
|
|
|
1846
|
+
// Any of the permissions (at least one required)
|
|
1847
|
+
app.bind(
|
|
1848
|
+
viewContentContract,
|
|
1849
|
+
[authenticate, requireAnyPermission('content:read', 'admin:access')],
|
|
1850
|
+
async (c) => {
|
|
1851
|
+
// User has either content:read OR admin:access
|
|
1852
|
+
}
|
|
1853
|
+
);
|
|
1854
|
+
|
|
1358
1855
|
// Role-based
|
|
1359
1856
|
app.bind(
|
|
1360
1857
|
adminDashboardContract,
|
|
@@ -1468,10 +1965,22 @@ import '@spfn/auth/nextjs/api';
|
|
|
1468
1965
|
**Target Routes:**
|
|
1469
1966
|
- `/_auth/login`, `/_auth/register` - Login/register interceptor
|
|
1470
1967
|
- `/_auth/keys/rotate` - Key rotation interceptor
|
|
1968
|
+
- `/_auth/oauth/:provider/url` - OAuth URL interceptor (keypair + state generation)
|
|
1969
|
+
- `/_auth/oauth/finalize` - OAuth finalize interceptor (pending session → full session)
|
|
1471
1970
|
- All other authenticated routes - General auth interceptor
|
|
1472
1971
|
|
|
1473
1972
|
---
|
|
1474
1973
|
|
|
1974
|
+
### OAuth Client Component (`@spfn/auth/nextjs/client`)
|
|
1975
|
+
|
|
1976
|
+
```typescript
|
|
1977
|
+
import { OAuthCallback, type OAuthCallbackProps } from '@spfn/auth/nextjs/client';
|
|
1978
|
+
```
|
|
1979
|
+
|
|
1980
|
+
OAuth 콜백 페이지용 `'use client'` 컴포넌트. 자세한 사용법은 [OAuth Authentication](#oauth-authentication) 섹션 참조.
|
|
1981
|
+
|
|
1982
|
+
---
|
|
1983
|
+
|
|
1475
1984
|
## Testing
|
|
1476
1985
|
|
|
1477
1986
|
### Setup Test Environment
|
|
@@ -1817,7 +2326,7 @@ ls migrations/
|
|
|
1817
2326
|
|
|
1818
2327
|
- [ ] **React hooks** - useAuth, useSession, usePermissions
|
|
1819
2328
|
- [ ] **UI components** - LoginForm, RegisterForm, AuthProvider
|
|
1820
|
-
- [
|
|
2329
|
+
- [x] **OAuth integration** - Google (implemented), GitHub/Kakao/Naver (planned)
|
|
1821
2330
|
- [ ] **2FA support** - TOTP/authenticator apps
|
|
1822
2331
|
- [ ] **Password reset flow** - Complete email-based reset
|
|
1823
2332
|
- [ ] **Email change flow** - Verification for email updates
|
|
@@ -1957,6 +2466,6 @@ MIT License - See LICENSE file for details.
|
|
|
1957
2466
|
|
|
1958
2467
|
---
|
|
1959
2468
|
|
|
1960
|
-
**Last Updated:**
|
|
1961
|
-
**Document Version:** 2.
|
|
1962
|
-
**Package Version:** 0.
|
|
2469
|
+
**Last Updated:** 2026-02-23
|
|
2470
|
+
**Document Version:** 2.6.0 (Technical Documentation)
|
|
2471
|
+
**Package Version:** 0.2.0-beta.15
|