@revealui/auth 0.2.0 → 0.3.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/README.md +58 -34
- package/dist/index.d.ts.map +1 -1
- package/dist/react/index.d.ts +4 -0
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/useMFA.d.ts +83 -0
- package/dist/react/useMFA.d.ts.map +1 -0
- package/dist/react/useMFA.js +182 -0
- package/dist/react/usePasskey.d.ts +88 -0
- package/dist/react/usePasskey.d.ts.map +1 -0
- package/dist/react/usePasskey.js +203 -0
- package/dist/react/useSession.d.ts.map +1 -1
- package/dist/react/useSession.js +16 -5
- package/dist/react/useSignIn.d.ts +9 -3
- package/dist/react/useSignIn.d.ts.map +1 -1
- package/dist/react/useSignIn.js +32 -10
- package/dist/react/useSignOut.d.ts.map +1 -1
- package/dist/react/useSignUp.d.ts +1 -0
- package/dist/react/useSignUp.d.ts.map +1 -1
- package/dist/react/useSignUp.js +25 -9
- package/dist/server/auth.d.ts +2 -0
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +93 -5
- package/dist/server/brute-force.d.ts +10 -1
- package/dist/server/brute-force.d.ts.map +1 -1
- package/dist/server/brute-force.js +46 -23
- package/dist/server/errors.d.ts +4 -0
- package/dist/server/errors.d.ts.map +1 -1
- package/dist/server/errors.js +8 -0
- package/dist/server/index.d.ts +17 -6
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +12 -5
- package/dist/server/magic-link.d.ts +52 -0
- package/dist/server/magic-link.d.ts.map +1 -0
- package/dist/server/magic-link.js +111 -0
- package/dist/server/mfa.d.ts +87 -0
- package/dist/server/mfa.d.ts.map +1 -0
- package/dist/server/mfa.js +263 -0
- package/dist/server/oauth.d.ts +86 -0
- package/dist/server/oauth.d.ts.map +1 -0
- package/dist/server/oauth.js +355 -0
- package/dist/server/passkey.d.ts +132 -0
- package/dist/server/passkey.d.ts.map +1 -0
- package/dist/server/passkey.js +257 -0
- package/dist/server/password-reset.d.ts +32 -6
- package/dist/server/password-reset.d.ts.map +1 -1
- package/dist/server/password-reset.js +116 -47
- package/dist/server/password-validation.d.ts.map +1 -1
- package/dist/server/providers/github.d.ts +14 -0
- package/dist/server/providers/github.d.ts.map +1 -0
- package/dist/server/providers/github.js +89 -0
- package/dist/server/providers/google.d.ts +11 -0
- package/dist/server/providers/google.d.ts.map +1 -0
- package/dist/server/providers/google.js +69 -0
- package/dist/server/providers/vercel.d.ts +11 -0
- package/dist/server/providers/vercel.d.ts.map +1 -0
- package/dist/server/providers/vercel.js +63 -0
- package/dist/server/rate-limit.d.ts +10 -1
- package/dist/server/rate-limit.d.ts.map +1 -1
- package/dist/server/rate-limit.js +61 -43
- package/dist/server/session.d.ts +48 -1
- package/dist/server/session.d.ts.map +1 -1
- package/dist/server/session.js +126 -7
- package/dist/server/signed-cookie.d.ts +32 -0
- package/dist/server/signed-cookie.d.ts.map +1 -0
- package/dist/server/signed-cookie.js +67 -0
- package/dist/server/storage/database.d.ts +10 -1
- package/dist/server/storage/database.d.ts.map +1 -1
- package/dist/server/storage/database.js +43 -5
- package/dist/server/storage/in-memory.d.ts +4 -0
- package/dist/server/storage/in-memory.d.ts.map +1 -1
- package/dist/server/storage/in-memory.js +16 -6
- package/dist/server/storage/index.d.ts +11 -3
- package/dist/server/storage/index.d.ts.map +1 -1
- package/dist/server/storage/index.js +18 -4
- package/dist/server/storage/interface.d.ts +11 -1
- package/dist/server/storage/interface.d.ts.map +1 -1
- package/dist/server/storage/interface.js +1 -1
- package/dist/types.d.ts +23 -8
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +2 -2
- package/dist/utils/database.d.ts.map +1 -1
- package/dist/utils/database.js +12 -2
- package/dist/utils/token.d.ts +9 -1
- package/dist/utils/token.d.ts.map +1 -1
- package/dist/utils/token.js +9 -1
- package/package.json +26 -8
package/README.md
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
# @revealui/auth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
See [Project Status](../../docs/PROJECT_STATUS.md) for framework readiness.
|
|
6
|
-
|
|
7
|
-
Authentication system for RevealUI - database-backed sessions with Better Auth patterns.
|
|
8
|
-
|
|
9
|
-
> **⚠️ Security Note:** Auth implementation exists but requires independent security audit before production use.
|
|
3
|
+
Session-based authentication for RevealUI — database-backed sessions, rate limiting, brute force protection, and password reset.
|
|
10
4
|
|
|
11
5
|
## Features
|
|
12
6
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
7
|
+
- **Database Sessions** — PostgreSQL/NeonDB-backed sessions with SHA-256 token hashing
|
|
8
|
+
- **Secure Cookies** — HTTP-only, SameSite, secure flag, cross-subdomain support
|
|
9
|
+
- **Rate Limiting** — Configurable per-endpoint rate limits stored in database
|
|
10
|
+
- **Brute Force Protection** — Progressive lockout on failed sign-in attempts
|
|
11
|
+
- **Password Reset** — Token-based password reset flow with email integration
|
|
12
|
+
- **Password Validation** — Strength requirements and common password checks
|
|
13
|
+
- **React Hooks** — Client-side session management (`useSession`, `useSignIn`, `useSignOut`)
|
|
14
|
+
- **Framework Agnostic** — Works with Next.js, Hono, and other Node.js frameworks
|
|
19
15
|
|
|
20
16
|
## Installation
|
|
21
17
|
|
|
@@ -25,36 +21,34 @@ pnpm add @revealui/auth
|
|
|
25
21
|
|
|
26
22
|
## Usage
|
|
27
23
|
|
|
28
|
-
### Server-
|
|
24
|
+
### Server-Side
|
|
29
25
|
|
|
30
26
|
```typescript
|
|
31
|
-
import { getSession } from '@revealui/auth/server'
|
|
32
|
-
import { type NextRequest, NextResponse } from 'next/server'
|
|
27
|
+
import { getSession, signIn, signOut, createSession } from '@revealui/auth/server'
|
|
33
28
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!session) {
|
|
38
|
-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
|
|
39
|
-
}
|
|
29
|
+
// Validate session from request headers
|
|
30
|
+
const session = await getSession(request.headers)
|
|
40
31
|
|
|
41
|
-
|
|
42
|
-
}
|
|
32
|
+
// Sign in with email/password
|
|
33
|
+
const result = await signIn({ email, password })
|
|
34
|
+
|
|
35
|
+
// Sign out (invalidate session)
|
|
36
|
+
await signOut(sessionToken)
|
|
43
37
|
```
|
|
44
38
|
|
|
45
|
-
### Client-
|
|
39
|
+
### Client-Side (React)
|
|
46
40
|
|
|
47
41
|
```typescript
|
|
48
42
|
'use client'
|
|
49
43
|
import { useSession, useSignIn, useSignOut } from '@revealui/auth/react'
|
|
50
44
|
|
|
51
|
-
function
|
|
45
|
+
function AuthComponent() {
|
|
52
46
|
const { data: session, isLoading } = useSession()
|
|
53
47
|
const { signIn } = useSignIn()
|
|
54
48
|
const { signOut } = useSignOut()
|
|
55
49
|
|
|
56
50
|
if (isLoading) return <div>Loading...</div>
|
|
57
|
-
if (!session) return <
|
|
51
|
+
if (!session) return <button onClick={() => signIn({ email, password })}>Sign In</button>
|
|
58
52
|
|
|
59
53
|
return (
|
|
60
54
|
<div>
|
|
@@ -65,13 +59,43 @@ function MyComponent() {
|
|
|
65
59
|
}
|
|
66
60
|
```
|
|
67
61
|
|
|
68
|
-
##
|
|
62
|
+
## Exports
|
|
63
|
+
|
|
64
|
+
| Subpath | Contents |
|
|
65
|
+
|---------|----------|
|
|
66
|
+
| `@revealui/auth/server` | Server-side auth (session CRUD, sign in/out, rate limiting, brute force) |
|
|
67
|
+
| `@revealui/auth/client` | Client-side utilities |
|
|
68
|
+
| `@revealui/auth/react` | React hooks (`useSession`, `useSignIn`, `useSignOut`) |
|
|
69
|
+
|
|
70
|
+
## Security
|
|
71
|
+
|
|
72
|
+
- Passwords hashed with bcrypt
|
|
73
|
+
- Session tokens hashed with SHA-256 before storage
|
|
74
|
+
- HTTP-only cookies prevent XSS token theft
|
|
75
|
+
- SameSite cookie attribute prevents CSRF
|
|
76
|
+
- Rate limiting prevents abuse (configurable per endpoint)
|
|
77
|
+
- Brute force protection with progressive lockout
|
|
78
|
+
- Cookie domain supports cross-subdomain auth (e.g. `.revealui.com`)
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
# Build
|
|
84
|
+
pnpm build
|
|
85
|
+
|
|
86
|
+
# Type check
|
|
87
|
+
pnpm typecheck
|
|
88
|
+
|
|
89
|
+
# Run tests
|
|
90
|
+
pnpm test
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Related
|
|
69
94
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- `POST /api/auth/sign-out` - Sign out
|
|
95
|
+
- [Core Package](../core/README.md) — CMS engine (uses auth for access control)
|
|
96
|
+
- [DB Package](../db/README.md) — Database schema (sessions, users, rate_limits tables)
|
|
97
|
+
- [Auth Guide](../../docs/AUTH.md) — Architecture, usage patterns, and security design
|
|
74
98
|
|
|
75
|
-
##
|
|
99
|
+
## License
|
|
76
100
|
|
|
77
|
-
|
|
101
|
+
MIT
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,cAAc,kBAAkB,CAAC;AAEjC,cAAc,mBAAmB,CAAC;AAGlC,YAAY,EACV,WAAW,EACX,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,IAAI,GACL,MAAM,YAAY,CAAC"}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -4,6 +4,10 @@
|
|
|
4
4
|
* Client-side React hooks for authentication.
|
|
5
5
|
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
6
|
*/
|
|
7
|
+
export type { MFASetupData, UseMFASetupResult, UseMFAVerifyResult, } from './useMFA.js';
|
|
8
|
+
export { useMFASetup, useMFAVerify } from './useMFA.js';
|
|
9
|
+
export type { PasskeyRegisterOptions, PasskeyRegisterResult, UsePasskeyRegisterResult, UsePasskeySignInResult, } from './usePasskey.js';
|
|
10
|
+
export { usePasskeyRegister, usePasskeySignIn } from './usePasskey.js';
|
|
7
11
|
export type { UseSessionResult } from './useSession.js';
|
|
8
12
|
export { useSession } from './useSession.js';
|
|
9
13
|
export type { SignInInput, UseSignInResult } from './useSignIn.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,YAAY,EACV,YAAY,EACZ,iBAAiB,EACjB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EACV,sBAAsB,EACtB,qBAAqB,EACrB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACvE,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACxD,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACnE,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/react/index.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* Client-side React hooks for authentication.
|
|
5
5
|
* Inspired by Better Auth and TanStack Start patterns.
|
|
6
6
|
*/
|
|
7
|
+
export { useMFASetup, useMFAVerify } from './useMFA.js';
|
|
8
|
+
export { usePasskeyRegister, usePasskeySignIn } from './usePasskey.js';
|
|
7
9
|
export { useSession } from './useSession.js';
|
|
8
10
|
export { useSignIn } from './useSignIn.js';
|
|
9
11
|
export { useSignOut } from './useSignOut.js';
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for Multi-Factor Authentication setup and verification.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* MFA setup data returned when initiating TOTP setup.
|
|
8
|
+
*/
|
|
9
|
+
export interface MFASetupData {
|
|
10
|
+
/** Base32-encoded TOTP secret */
|
|
11
|
+
secret: string;
|
|
12
|
+
/** otpauth:// URI for QR code generation */
|
|
13
|
+
uri: string;
|
|
14
|
+
/** One-time backup codes for account recovery */
|
|
15
|
+
backupCodes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface UseMFASetupResult {
|
|
18
|
+
/** Initiate MFA setup — returns secret, QR URI, and backup codes */
|
|
19
|
+
setup: () => Promise<MFASetupData | null>;
|
|
20
|
+
/** Verify a TOTP code to confirm setup */
|
|
21
|
+
verifySetup: (code: string) => Promise<boolean>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Hook for MFA setup on the security settings page.
|
|
27
|
+
*
|
|
28
|
+
* @returns Setup and verify functions, loading state, and error
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```tsx
|
|
32
|
+
* function SecuritySettings() {
|
|
33
|
+
* const { setup, verifySetup, isLoading, error } = useMFASetup();
|
|
34
|
+
*
|
|
35
|
+
* const handleEnable = async () => {
|
|
36
|
+
* const data = await setup();
|
|
37
|
+
* if (data) {
|
|
38
|
+
* // Show QR code using data.uri, display backup codes
|
|
39
|
+
* }
|
|
40
|
+
* };
|
|
41
|
+
*
|
|
42
|
+
* const handleVerify = async (code: string) => {
|
|
43
|
+
* const success = await verifySetup(code);
|
|
44
|
+
* if (success) {
|
|
45
|
+
* // MFA is now enabled
|
|
46
|
+
* }
|
|
47
|
+
* };
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export declare function useMFASetup(): UseMFASetupResult;
|
|
52
|
+
export interface UseMFAVerifyResult {
|
|
53
|
+
/** Verify a TOTP code during login */
|
|
54
|
+
verify: (code: string) => Promise<boolean>;
|
|
55
|
+
/** Verify a backup code during login */
|
|
56
|
+
verifyBackupCode: (code: string) => Promise<boolean>;
|
|
57
|
+
isLoading: boolean;
|
|
58
|
+
error: string | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Hook for MFA verification during the login flow.
|
|
62
|
+
*
|
|
63
|
+
* After sign-in returns `requiresMfa: true`, redirect to the MFA page
|
|
64
|
+
* and use this hook to complete authentication.
|
|
65
|
+
*
|
|
66
|
+
* @returns Verify functions, loading state, and error
|
|
67
|
+
*
|
|
68
|
+
* @example
|
|
69
|
+
* ```tsx
|
|
70
|
+
* function MFAPage() {
|
|
71
|
+
* const { verify, verifyBackupCode, isLoading, error } = useMFAVerify();
|
|
72
|
+
*
|
|
73
|
+
* const handleSubmit = async (code: string) => {
|
|
74
|
+
* const success = await verify(code);
|
|
75
|
+
* if (success) {
|
|
76
|
+
* router.push('/admin');
|
|
77
|
+
* }
|
|
78
|
+
* };
|
|
79
|
+
* }
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function useMFAVerify(): UseMFAVerifyResult;
|
|
83
|
+
//# sourceMappingURL=useMFA.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useMFA.d.ts","sourceRoot":"","sources":["../../src/react/useMFA.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAMH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,iCAAiC;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,GAAG,EAAE,MAAM,CAAC;IACZ,iDAAiD;IACjD,WAAW,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IAChC,oEAAoE;IACpE,KAAK,EAAE,MAAM,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC1C,0CAA0C;IAC1C,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAChD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,WAAW,IAAI,iBAAiB,CAqE/C;AAED,MAAM,WAAW,kBAAkB;IACjC,sCAAsC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,wCAAwC;IACxC,gBAAgB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,IAAI,kBAAkB,CAsEjD"}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MFA Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for Multi-Factor Authentication setup and verification.
|
|
5
|
+
*/
|
|
6
|
+
'use client';
|
|
7
|
+
import { useState } from 'react';
|
|
8
|
+
/**
|
|
9
|
+
* Hook for MFA setup on the security settings page.
|
|
10
|
+
*
|
|
11
|
+
* @returns Setup and verify functions, loading state, and error
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* function SecuritySettings() {
|
|
16
|
+
* const { setup, verifySetup, isLoading, error } = useMFASetup();
|
|
17
|
+
*
|
|
18
|
+
* const handleEnable = async () => {
|
|
19
|
+
* const data = await setup();
|
|
20
|
+
* if (data) {
|
|
21
|
+
* // Show QR code using data.uri, display backup codes
|
|
22
|
+
* }
|
|
23
|
+
* };
|
|
24
|
+
*
|
|
25
|
+
* const handleVerify = async (code: string) => {
|
|
26
|
+
* const success = await verifySetup(code);
|
|
27
|
+
* if (success) {
|
|
28
|
+
* // MFA is now enabled
|
|
29
|
+
* }
|
|
30
|
+
* };
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useMFASetup() {
|
|
35
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
36
|
+
const [error, setError] = useState(null);
|
|
37
|
+
const setup = async () => {
|
|
38
|
+
try {
|
|
39
|
+
setIsLoading(true);
|
|
40
|
+
setError(null);
|
|
41
|
+
const response = await fetch('/api/auth/mfa/setup', {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
credentials: 'include',
|
|
44
|
+
});
|
|
45
|
+
const json = await response.json();
|
|
46
|
+
if (!response.ok) {
|
|
47
|
+
const errorData = json;
|
|
48
|
+
setError(errorData.error ?? 'Failed to set up MFA');
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
const data = json;
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
56
|
+
setError(message);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
setIsLoading(false);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const verifySetup = async (code) => {
|
|
64
|
+
try {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setError(null);
|
|
67
|
+
const response = await fetch('/api/auth/mfa/verify-setup', {
|
|
68
|
+
method: 'POST',
|
|
69
|
+
headers: { 'Content-Type': 'application/json' },
|
|
70
|
+
credentials: 'include',
|
|
71
|
+
body: JSON.stringify({ code }),
|
|
72
|
+
});
|
|
73
|
+
const json = await response.json();
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
const errorData = json;
|
|
76
|
+
setError(errorData.error ?? 'Failed to verify MFA code');
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
setError(message);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
setIsLoading(false);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
return {
|
|
91
|
+
setup,
|
|
92
|
+
verifySetup,
|
|
93
|
+
isLoading,
|
|
94
|
+
error,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Hook for MFA verification during the login flow.
|
|
99
|
+
*
|
|
100
|
+
* After sign-in returns `requiresMfa: true`, redirect to the MFA page
|
|
101
|
+
* and use this hook to complete authentication.
|
|
102
|
+
*
|
|
103
|
+
* @returns Verify functions, loading state, and error
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* function MFAPage() {
|
|
108
|
+
* const { verify, verifyBackupCode, isLoading, error } = useMFAVerify();
|
|
109
|
+
*
|
|
110
|
+
* const handleSubmit = async (code: string) => {
|
|
111
|
+
* const success = await verify(code);
|
|
112
|
+
* if (success) {
|
|
113
|
+
* router.push('/admin');
|
|
114
|
+
* }
|
|
115
|
+
* };
|
|
116
|
+
* }
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
export function useMFAVerify() {
|
|
120
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
121
|
+
const [error, setError] = useState(null);
|
|
122
|
+
const verify = async (code) => {
|
|
123
|
+
try {
|
|
124
|
+
setIsLoading(true);
|
|
125
|
+
setError(null);
|
|
126
|
+
const response = await fetch('/api/auth/mfa/verify', {
|
|
127
|
+
method: 'POST',
|
|
128
|
+
headers: { 'Content-Type': 'application/json' },
|
|
129
|
+
credentials: 'include',
|
|
130
|
+
body: JSON.stringify({ code }),
|
|
131
|
+
});
|
|
132
|
+
const json = await response.json();
|
|
133
|
+
if (!response.ok) {
|
|
134
|
+
const errorData = json;
|
|
135
|
+
setError(errorData.error ?? 'Invalid verification code');
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
142
|
+
setError(message);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
finally {
|
|
146
|
+
setIsLoading(false);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
const verifyBackupCode = async (code) => {
|
|
150
|
+
try {
|
|
151
|
+
setIsLoading(true);
|
|
152
|
+
setError(null);
|
|
153
|
+
const response = await fetch('/api/auth/mfa/backup', {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers: { 'Content-Type': 'application/json' },
|
|
156
|
+
credentials: 'include',
|
|
157
|
+
body: JSON.stringify({ code }),
|
|
158
|
+
});
|
|
159
|
+
const json = await response.json();
|
|
160
|
+
if (!response.ok) {
|
|
161
|
+
const errorData = json;
|
|
162
|
+
setError(errorData.error ?? 'Invalid backup code');
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
169
|
+
setError(message);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
finally {
|
|
173
|
+
setIsLoading(false);
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
return {
|
|
177
|
+
verify,
|
|
178
|
+
verifyBackupCode,
|
|
179
|
+
isLoading,
|
|
180
|
+
error,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Passkey Hooks
|
|
3
|
+
*
|
|
4
|
+
* React hooks for WebAuthn passkey registration and authentication.
|
|
5
|
+
* Uses dynamic imports for @simplewebauthn/browser to avoid SSR issues.
|
|
6
|
+
*/
|
|
7
|
+
export interface PasskeyRegisterOptions {
|
|
8
|
+
/** Email for passkey registration (sign-up flow) */
|
|
9
|
+
email?: string;
|
|
10
|
+
/** Display name for the credential */
|
|
11
|
+
name?: string;
|
|
12
|
+
/** Human-readable device name (e.g., "MacBook Pro") */
|
|
13
|
+
deviceName?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface PasskeyRegisterResult {
|
|
16
|
+
/** Backup codes returned during sign-up flow */
|
|
17
|
+
backupCodes?: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface UsePasskeyRegisterResult {
|
|
20
|
+
/** Register a new passkey credential */
|
|
21
|
+
register: (options?: PasskeyRegisterOptions) => Promise<PasskeyRegisterResult | null>;
|
|
22
|
+
isLoading: boolean;
|
|
23
|
+
error: string | null;
|
|
24
|
+
/** Whether the browser supports WebAuthn */
|
|
25
|
+
supported: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Hook for passkey registration (sign-up and security settings).
|
|
29
|
+
*
|
|
30
|
+
* @returns Register function, loading state, error, and browser support flag
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```tsx
|
|
34
|
+
* function AddPasskey() {
|
|
35
|
+
* const { register, isLoading, error, supported } = usePasskeyRegister();
|
|
36
|
+
*
|
|
37
|
+
* if (!supported) return <p>Passkeys are not supported in this browser.</p>;
|
|
38
|
+
*
|
|
39
|
+
* const handleAdd = async () => {
|
|
40
|
+
* const result = await register({ deviceName: 'My Laptop' });
|
|
41
|
+
* if (result) {
|
|
42
|
+
* // Passkey registered successfully
|
|
43
|
+
* }
|
|
44
|
+
* };
|
|
45
|
+
* }
|
|
46
|
+
* ```
|
|
47
|
+
*/
|
|
48
|
+
export declare function usePasskeyRegister(): UsePasskeyRegisterResult;
|
|
49
|
+
export interface UsePasskeySignInResult {
|
|
50
|
+
/** Authenticate with a passkey */
|
|
51
|
+
signIn: () => Promise<boolean>;
|
|
52
|
+
isLoading: boolean;
|
|
53
|
+
error: string | null;
|
|
54
|
+
/** Whether the browser supports WebAuthn */
|
|
55
|
+
supported: boolean;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Hook for passkey authentication (login page).
|
|
59
|
+
*
|
|
60
|
+
* @returns Sign-in function, loading state, error, and browser support flag
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```tsx
|
|
64
|
+
* function LoginPage() {
|
|
65
|
+
* const { signIn, isLoading, error, supported } = usePasskeySignIn();
|
|
66
|
+
*
|
|
67
|
+
* const handlePasskeyLogin = async () => {
|
|
68
|
+
* const success = await signIn();
|
|
69
|
+
* if (success) {
|
|
70
|
+
* router.push('/admin');
|
|
71
|
+
* }
|
|
72
|
+
* };
|
|
73
|
+
*
|
|
74
|
+
* return (
|
|
75
|
+
* <>
|
|
76
|
+
* {supported && (
|
|
77
|
+
* <button onClick={handlePasskeyLogin} disabled={isLoading}>
|
|
78
|
+
* Sign in with Passkey
|
|
79
|
+
* </button>
|
|
80
|
+
* )}
|
|
81
|
+
* {error && <p>{error}</p>}
|
|
82
|
+
* </>
|
|
83
|
+
* );
|
|
84
|
+
* }
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
export declare function usePasskeySignIn(): UsePasskeySignInResult;
|
|
88
|
+
//# sourceMappingURL=usePasskey.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"usePasskey.d.ts","sourceRoot":"","sources":["../../src/react/usePasskey.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,WAAW,sBAAsB;IACrC,oDAAoD;IACpD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sCAAsC;IACtC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uDAAuD;IACvD,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,qBAAqB;IACpC,gDAAgD;IAChD,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,MAAM,WAAW,wBAAwB;IACvC,wCAAwC;IACxC,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,sBAAsB,KAAK,OAAO,CAAC,qBAAqB,GAAG,IAAI,CAAC,CAAC;IACtF,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,kBAAkB,IAAI,wBAAwB,CA0F7D;AAED,MAAM,WAAW,sBAAsB;IACrC,kCAAkC;IAClC,MAAM,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,4CAA4C;IAC5C,SAAS,EAAE,OAAO,CAAC;CACpB;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,gBAAgB,IAAI,sBAAsB,CA8EzD"}
|