create-githat-app 1.8.3 → 1.8.6

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.
Files changed (46) hide show
  1. package/dist/cli.js +2 -2
  2. package/package.json +1 -1
  3. package/templates/agent/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  4. package/templates/agent/app/account/activity/page.tsx.hbs +176 -0
  5. package/templates/agent/app/account/security/page.tsx.hbs +46 -0
  6. package/templates/agent/app/account/sessions/page.tsx.hbs +180 -0
  7. package/templates/base/README.md.hbs +102 -0
  8. package/templates/classroom/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  9. package/templates/classroom/app/account/activity/page.tsx.hbs +176 -0
  10. package/templates/classroom/app/account/security/page.tsx.hbs +46 -0
  11. package/templates/classroom/app/account/sessions/page.tsx.hbs +180 -0
  12. package/templates/content/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  13. package/templates/content/app/account/activity/page.tsx.hbs +176 -0
  14. package/templates/content/app/account/security/page.tsx.hbs +46 -0
  15. package/templates/content/app/account/sessions/page.tsx.hbs +180 -0
  16. package/templates/dashboard/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  17. package/templates/dashboard/app/account/activity/page.tsx.hbs +176 -0
  18. package/templates/dashboard/app/account/security/page.tsx.hbs +46 -0
  19. package/templates/dashboard/app/account/sessions/page.tsx.hbs +180 -0
  20. package/templates/marketplace/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  21. package/templates/marketplace/app/account/activity/page.tsx.hbs +176 -0
  22. package/templates/marketplace/app/account/security/page.tsx.hbs +46 -0
  23. package/templates/marketplace/app/account/sessions/page.tsx.hbs +180 -0
  24. package/templates/marketplace/app/admin/page.tsx.hbs +19 -19
  25. package/templates/marketplace/app/cart/page.tsx.hbs +23 -21
  26. package/templates/marketplace/app/layout.tsx.hbs +7 -7
  27. package/templates/marketplace/app/page.tsx.hbs +82 -31
  28. package/templates/marketplace/app/sell/page.tsx.hbs +17 -18
  29. package/templates/marketplace/src/data/products.ts.hbs +64 -0
  30. package/templates/marketplace/src/lib/categories.ts.hbs +17 -23
  31. package/templates/nextjs/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  32. package/templates/nextjs/app/account/activity/page.tsx.hbs +176 -0
  33. package/templates/nextjs/app/account/security/page.tsx.hbs +46 -0
  34. package/templates/nextjs/app/account/sessions/page.tsx.hbs +180 -0
  35. package/templates/plain/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  36. package/templates/plain/app/account/activity/page.tsx.hbs +176 -0
  37. package/templates/plain/app/account/security/page.tsx.hbs +46 -0
  38. package/templates/plain/app/account/sessions/page.tsx.hbs +180 -0
  39. package/templates/portfolio/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  40. package/templates/portfolio/app/account/activity/page.tsx.hbs +176 -0
  41. package/templates/portfolio/app/account/security/page.tsx.hbs +46 -0
  42. package/templates/portfolio/app/account/sessions/page.tsx.hbs +180 -0
  43. package/templates/saas/app/(auth)/sign-in/magic/page.tsx.hbs +11 -0
  44. package/templates/saas/app/account/activity/page.tsx.hbs +176 -0
  45. package/templates/saas/app/account/security/page.tsx.hbs +46 -0
  46. package/templates/saas/app/account/sessions/page.tsx.hbs +180 -0
package/dist/cli.js CHANGED
@@ -21,7 +21,7 @@ var DEPS = {
21
21
  next: "^16.0.0",
22
22
  react: "^19.0.0",
23
23
  "react-dom": "^19.0.0",
24
- "@githat/nextjs": "^0.13.0",
24
+ "@githat/nextjs": "^0.13.3",
25
25
  "@githat/ui": "^1.0.0"
26
26
  },
27
27
  devDependencies: {
@@ -36,7 +36,7 @@ var DEPS = {
36
36
  react: "^19.0.0",
37
37
  "react-dom": "^19.0.0",
38
38
  "react-router-dom": "^7.0.0",
39
- "@githat/nextjs": "^0.13.0",
39
+ "@githat/nextjs": "^0.13.3",
40
40
  "@githat/ui": "^1.0.0"
41
41
  },
42
42
  devDependencies: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-githat-app",
3
- "version": "1.8.3",
3
+ "version": "1.8.6",
4
4
  "description": "GitHat CLI — scaffold apps and manage the skills marketplace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { Suspense } from 'react';
3
+ import { MagicLinkVerify } from '@githat/nextjs';
4
+
5
+ export default function MagicLinkPage() {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <MagicLinkVerify />
9
+ </Suspense>
10
+ );
11
+ }
@@ -0,0 +1,176 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useAuditLog } from '@githat/nextjs';
5
+ import type { AuditEvent, AuditEventType } from '@githat/nextjs';
6
+
7
+ const EVENT_ICONS: Record<string, string> = {
8
+ login_success: '✓',
9
+ login_failed: '✗',
10
+ mfa_enabled: '🔒',
11
+ mfa_disabled: '🔓',
12
+ mfa_challenge_failed: '⚠',
13
+ password_changed: '🔑',
14
+ email_verified: '✉',
15
+ passkey_added: '🔐',
16
+ passkey_removed: '🗑',
17
+ magic_link_requested: '✉',
18
+ session_revoked: '⊗',
19
+ account_locked: '⛔',
20
+ user_created: '★',
21
+ };
22
+
23
+ const EVENT_LABELS: Record<string, string> = {
24
+ login_success: 'Signed in',
25
+ login_failed: 'Failed sign-in attempt',
26
+ mfa_enabled: '2FA enabled',
27
+ mfa_disabled: '2FA disabled',
28
+ mfa_challenge_failed: '2FA challenge failed',
29
+ password_changed: 'Password changed',
30
+ email_verified: 'Email verified',
31
+ passkey_added: 'Passkey added',
32
+ passkey_removed: 'Passkey removed',
33
+ magic_link_requested: 'Magic link sent',
34
+ session_revoked: 'Session revoked',
35
+ account_locked: 'Account locked',
36
+ user_created: 'Account created',
37
+ };
38
+
39
+ /**
40
+ * /account/activity — Sign-in audit log for {{businessName}}.
41
+ *
42
+ * Immutable per-user record of auth events. Paginated table view
43
+ * with type icon, when, IP, user-agent, and country flag.
44
+ */
45
+ export default function ActivityPage() {
46
+ const { list } = useAuditLog();
47
+ const [events, setEvents] = useState<AuditEvent[]>([]);
48
+ const [loading, setLoading] = useState(true);
49
+ const [error, setError] = useState<string | null>(null);
50
+ const [cursor, setCursor] = useState<string | null>(null);
51
+ const [loadingMore, setLoadingMore] = useState(false);
52
+
53
+ async function load(append = false) {
54
+ if (append) setLoadingMore(true); else setLoading(true);
55
+ setError(null);
56
+ try {
57
+ const result = await list({ limit: 50, ...(append && cursor ? { cursor } : {}) });
58
+ setEvents((prev) => append ? [...prev, ...result.events] : result.events);
59
+ setCursor(result.nextCursor);
60
+ } catch (err: unknown) {
61
+ setError(err instanceof Error ? err.message : 'Failed to load activity');
62
+ } finally {
63
+ if (append) setLoadingMore(false); else setLoading(false);
64
+ }
65
+ }
66
+
67
+ useEffect(() => { load(); }, []);
68
+
69
+ return (
70
+ <main
71
+ style=\{{
72
+ maxWidth: '900px',
73
+ margin: '0 auto',
74
+ padding: 'var(--space-8, 2rem) var(--space-4, 1rem)',
75
+ color: 'var(--fg, #111)',
76
+ }}
77
+ >
78
+ <header style=\{{ marginBottom: '1.5rem' }}>
79
+ <div style=\{{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
80
+ <a href="/account/security" style=\{{ color: 'var(--fg-muted, #666)', textDecoration: 'none', fontSize: '0.875rem' }}>
81
+ Security
82
+ </a>
83
+ <span style=\{{ color: 'var(--fg-muted, #666)' }}>›</span>
84
+ <span style=\{{ fontSize: '0.875rem' }}>Activity</span>
85
+ </div>
86
+ <h1 style=\{{ fontSize: '1.875rem', fontWeight: 700, margin: 0 }}>Sign-in activity</h1>
87
+ <p style=\{{ color: 'var(--fg-muted, #666)', marginTop: '0.25rem' }}>
88
+ Immutable record of security events for your {{businessName}} account.
89
+ </p>
90
+ </header>
91
+
92
+ {error && (
93
+ <div style=\{{ padding: '0.75rem 1rem', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: '6px', color: '#dc2626', marginBottom: '1rem' }}>
94
+ {error}
95
+ </div>
96
+ )}
97
+
98
+ {loading ? (
99
+ <p style=\{{ color: 'var(--fg-muted, #666)' }}>Loading activity...</p>
100
+ ) : events.length === 0 ? (
101
+ <p style=\{{ color: 'var(--fg-muted, #666)' }}>No activity found.</p>
102
+ ) : (
103
+ <>
104
+ <div style=\{{ overflowX: 'auto' }}>
105
+ <table style=\{{ width: '100%', borderCollapse: 'collapse', fontSize: '0.875rem' }}>
106
+ <thead>
107
+ <tr style=\{{ borderBottom: '1px solid var(--border, #e5e7eb)', textAlign: 'left' }}>
108
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600, color: 'var(--fg-muted, #666)', width: '2rem' }}></th>
109
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600 }}>Event</th>
110
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600 }}>When</th>
111
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600 }}>IP</th>
112
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600 }}>Country</th>
113
+ <th style=\{{ padding: '0.5rem 0.75rem', fontWeight: 600 }}>Device</th>
114
+ </tr>
115
+ </thead>
116
+ <tbody>
117
+ {events.map((evt, i) => {
118
+ const meta = evt.metadata as Record<string, string | undefined>;
119
+ const ua = meta?.userAgent || '';
120
+ const truncatedUa = ua.length > 50 ? ua.slice(0, 50) + '…' : ua;
121
+ return (
122
+ <tr
123
+ key={evt.sk || i}
124
+ style=\{{ borderBottom: '1px solid var(--border, #e5e7eb)' }}
125
+ >
126
+ <td style=\{{ padding: '0.6rem 0.75rem', textAlign: 'center', fontSize: '1rem' }}>
127
+ {EVENT_ICONS[evt.action] || '•'}
128
+ </td>
129
+ <td style=\{{ padding: '0.6rem 0.75rem', fontWeight: 500 }}>
130
+ {EVENT_LABELS[evt.action] || evt.action}
131
+ </td>
132
+ <td style=\{{ padding: '0.6rem 0.75rem', color: 'var(--fg-muted, #666)', whiteSpace: 'nowrap' }}>
133
+ {new Date(evt.created_at).toLocaleString()}
134
+ </td>
135
+ <td style=\{{ padding: '0.6rem 0.75rem', color: 'var(--fg-muted, #666)', fontFamily: 'monospace' }}>
136
+ {meta?.ipAddress || '—'}
137
+ </td>
138
+ <td style=\{{ padding: '0.6rem 0.75rem', color: 'var(--fg-muted, #666)' }}>
139
+ {meta?.country || '—'}
140
+ </td>
141
+ <td
142
+ style=\{{ padding: '0.6rem 0.75rem', color: 'var(--fg-muted, #666)', maxWidth: '280px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}
143
+ title={ua}
144
+ >
145
+ {truncatedUa || '—'}
146
+ </td>
147
+ </tr>
148
+ );
149
+ })}
150
+ </tbody>
151
+ </table>
152
+ </div>
153
+
154
+ {cursor && (
155
+ <div style=\{{ marginTop: '1.5rem', textAlign: 'center' }}>
156
+ <button
157
+ onClick={() => load(true)}
158
+ disabled={loadingMore}
159
+ style=\{{
160
+ padding: '0.5rem 1.5rem',
161
+ background: 'transparent',
162
+ border: '1px solid var(--border, #e5e7eb)',
163
+ borderRadius: '6px',
164
+ cursor: loadingMore ? 'not-allowed' : 'pointer',
165
+ fontSize: '0.875rem',
166
+ }}
167
+ >
168
+ {loadingMore ? 'Loading...' : 'Load more'}
169
+ </button>
170
+ </div>
171
+ )}
172
+ </>
173
+ )}
174
+ </main>
175
+ );
176
+ }
@@ -26,7 +26,53 @@ export default function SecurityPage() {
26
26
  Manage two-factor authentication for your {{businessName}} account.
27
27
  </p>
28
28
  </header>
29
+
29
30
  <MfaManager />
31
+
32
+ <nav style=\{{ marginTop: '2.5rem', display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
33
+ <a
34
+ href="/account/sessions"
35
+ style=\{{
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ justifyContent: 'space-between',
39
+ padding: '0.875rem 1.25rem',
40
+ border: '1px solid var(--border, #e5e7eb)',
41
+ borderRadius: '8px',
42
+ textDecoration: 'none',
43
+ color: 'var(--fg, #111)',
44
+ }}
45
+ >
46
+ <div>
47
+ <div style=\{{ fontWeight: 600, fontSize: '0.9rem' }}>Active sessions</div>
48
+ <div style=\{{ fontSize: '0.8rem', color: 'var(--fg-muted, #666)', marginTop: '0.125rem' }}>
49
+ See and revoke devices signed in to your account
50
+ </div>
51
+ </div>
52
+ <span style=\{{ color: 'var(--fg-muted, #666)' }}>›</span>
53
+ </a>
54
+ <a
55
+ href="/account/activity"
56
+ style=\{{
57
+ display: 'flex',
58
+ alignItems: 'center',
59
+ justifyContent: 'space-between',
60
+ padding: '0.875rem 1.25rem',
61
+ border: '1px solid var(--border, #e5e7eb)',
62
+ borderRadius: '8px',
63
+ textDecoration: 'none',
64
+ color: 'var(--fg, #111)',
65
+ }}
66
+ >
67
+ <div>
68
+ <div style=\{{ fontWeight: 600, fontSize: '0.9rem' }}>Sign-in activity</div>
69
+ <div style=\{{ fontSize: '0.8rem', color: 'var(--fg-muted, #666)', marginTop: '0.125rem' }}>
70
+ Review your account&apos;s sign-in history
71
+ </div>
72
+ </div>
73
+ <span style=\{{ color: 'var(--fg-muted, #666)' }}>›</span>
74
+ </a>
75
+ </nav>
30
76
  </main>
31
77
  );
32
78
  }
@@ -0,0 +1,180 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useState } from 'react';
4
+ import { useSessions } from '@githat/nextjs';
5
+ import type { Session } from '@githat/nextjs';
6
+
7
+ /**
8
+ * /account/sessions — Active session management for {{businessName}}.
9
+ *
10
+ * Lists all active sessions with device/IP/country info.
11
+ * Users can revoke individual sessions or sign out everywhere else.
12
+ */
13
+ export default function SessionsPage() {
14
+ const { list, revoke, revokeAll } = useSessions();
15
+ const [sessions, setSessions] = useState<Session[]>([]);
16
+ const [loading, setLoading] = useState(true);
17
+ const [error, setError] = useState<string | null>(null);
18
+ const [revoking, setRevoking] = useState<string | null>(null);
19
+ const [revokingAll, setRevokingAll] = useState(false);
20
+
21
+ async function load() {
22
+ setLoading(true);
23
+ setError(null);
24
+ try {
25
+ const { sessions: data } = await list();
26
+ setSessions(data);
27
+ } catch (err: unknown) {
28
+ setError(err instanceof Error ? err.message : 'Failed to load sessions');
29
+ } finally {
30
+ setLoading(false);
31
+ }
32
+ }
33
+
34
+ useEffect(() => { load(); }, []);
35
+
36
+ async function handleRevoke(sessionId: string) {
37
+ setRevoking(sessionId);
38
+ try {
39
+ await revoke(sessionId);
40
+ setSessions((prev) => prev.filter((s) => s.id !== sessionId));
41
+ } catch (err: unknown) {
42
+ setError(err instanceof Error ? err.message : 'Failed to revoke session');
43
+ } finally {
44
+ setRevoking(null);
45
+ }
46
+ }
47
+
48
+ async function handleRevokeAll() {
49
+ if (!confirm('Sign out of all other devices?')) return;
50
+ setRevokingAll(true);
51
+ try {
52
+ await revokeAll();
53
+ setSessions((prev) => prev.filter((s) => s.current));
54
+ } catch (err: unknown) {
55
+ setError(err instanceof Error ? err.message : 'Failed to revoke sessions');
56
+ } finally {
57
+ setRevokingAll(false);
58
+ }
59
+ }
60
+
61
+ const otherCount = sessions.filter((s) => !s.current).length;
62
+
63
+ return (
64
+ <main
65
+ style=\{{
66
+ maxWidth: '720px',
67
+ margin: '0 auto',
68
+ padding: 'var(--space-8, 2rem) var(--space-4, 1rem)',
69
+ color: 'var(--fg, #111)',
70
+ }}
71
+ >
72
+ <header style=\{{ marginBottom: '1.5rem' }}>
73
+ <div style=\{{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
74
+ <a href="/account/security" style=\{{ color: 'var(--fg-muted, #666)', textDecoration: 'none', fontSize: '0.875rem' }}>
75
+ Security
76
+ </a>
77
+ <span style=\{{ color: 'var(--fg-muted, #666)' }}>›</span>
78
+ <span style=\{{ fontSize: '0.875rem' }}>Active sessions</span>
79
+ </div>
80
+ <h1 style=\{{ fontSize: '1.875rem', fontWeight: 700, margin: 0 }}>Active sessions</h1>
81
+ <p style=\{{ color: 'var(--fg-muted, #666)', marginTop: '0.25rem' }}>
82
+ Devices signed in to your {{businessName}} account.
83
+ </p>
84
+ </header>
85
+
86
+ {error && (
87
+ <div style=\{{ padding: '0.75rem 1rem', background: '#fef2f2', border: '1px solid #fecaca', borderRadius: '6px', color: '#dc2626', marginBottom: '1rem' }}>
88
+ {error}
89
+ </div>
90
+ )}
91
+
92
+ {otherCount > 0 && (
93
+ <div style=\{{ marginBottom: '1.5rem' }}>
94
+ <button
95
+ onClick={handleRevokeAll}
96
+ disabled={revokingAll}
97
+ style=\{{
98
+ padding: '0.5rem 1.25rem',
99
+ background: '#fef2f2',
100
+ color: '#dc2626',
101
+ border: '1px solid #fecaca',
102
+ borderRadius: '6px',
103
+ cursor: revokingAll ? 'not-allowed' : 'pointer',
104
+ fontWeight: 500,
105
+ }}
106
+ >
107
+ {revokingAll ? 'Signing out...' : `Sign out everywhere else (${otherCount})`}
108
+ </button>
109
+ </div>
110
+ )}
111
+
112
+ {loading ? (
113
+ <p style=\{{ color: 'var(--fg-muted, #666)' }}>Loading sessions...</p>
114
+ ) : sessions.length === 0 ? (
115
+ <p style=\{{ color: 'var(--fg-muted, #666)' }}>No active sessions found.</p>
116
+ ) : (
117
+ <div style=\{{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
118
+ {sessions.map((session) => (
119
+ <div
120
+ key={session.id}
121
+ style=\{{
122
+ padding: '1rem 1.25rem',
123
+ border: session.current ? '1px solid #6366f1' : '1px solid var(--border, #e5e7eb)',
124
+ borderRadius: '8px',
125
+ display: 'flex',
126
+ justifyContent: 'space-between',
127
+ alignItems: 'flex-start',
128
+ gap: '1rem',
129
+ }}
130
+ >
131
+ <div style=\{{ flex: 1, minWidth: 0 }}>
132
+ <div style=\{{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginBottom: '0.25rem' }}>
133
+ <span style=\{{ fontWeight: 600, fontSize: '0.9rem', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
134
+ {session.userAgent || 'Unknown device'}
135
+ </span>
136
+ {session.current && (
137
+ <span style=\{{
138
+ padding: '0.1rem 0.5rem',
139
+ background: '#ede9fe',
140
+ color: '#6366f1',
141
+ borderRadius: '999px',
142
+ fontSize: '0.75rem',
143
+ fontWeight: 600,
144
+ whiteSpace: 'nowrap',
145
+ }}>
146
+ This device
147
+ </span>
148
+ )}
149
+ </div>
150
+ <div style=\{{ fontSize: '0.8rem', color: 'var(--fg-muted, #666)', display: 'flex', gap: '0.75rem', flexWrap: 'wrap' }}>
151
+ {session.ipAddress && <span>{session.ipAddress}</span>}
152
+ {session.country && <span>{session.country}</span>}
153
+ <span>Last active {new Date(session.lastActiveAt).toLocaleDateString()}</span>
154
+ </div>
155
+ </div>
156
+ {!session.current && (
157
+ <button
158
+ onClick={() => handleRevoke(session.id)}
159
+ disabled={revoking === session.id}
160
+ style=\{{
161
+ padding: '0.375rem 0.875rem',
162
+ background: 'transparent',
163
+ border: '1px solid var(--border, #e5e7eb)',
164
+ borderRadius: '6px',
165
+ cursor: revoking === session.id ? 'not-allowed' : 'pointer',
166
+ fontSize: '0.875rem',
167
+ whiteSpace: 'nowrap',
168
+ flexShrink: 0,
169
+ }}
170
+ >
171
+ {revoking === session.id ? 'Revoking...' : 'Revoke'}
172
+ </button>
173
+ )}
174
+ </div>
175
+ ))}
176
+ </div>
177
+ )}
178
+ </main>
179
+ );
180
+ }
@@ -100,6 +100,44 @@ const domains = await list(); // includes verificationStatus
100
100
  Once verified, links in emails point to `https://{{domain}}/reset-password?token=…`
101
101
  — users never see githat.io.
102
102
 
103
+ ## Webhooks
104
+
105
+ Subscribe to GitHat auth events from your backend via HTTP callbacks. Use the
106
+ GitHat dashboard (`/webhooks`) or the `useWebhooks()` hook to register an endpoint:
107
+
108
+ ```ts
109
+ import { useWebhooks } from '@githat/nextjs';
110
+
111
+ const { create, list, listDeliveries } = useWebhooks();
112
+ const webhook = await create('https://yourapp.com/hooks/githat', ['user.signed_in', 'user.created']);
113
+ // Save webhook.secret — it's shown only once and used to verify HMAC signatures.
114
+ ```
115
+
116
+ Each delivery sends `X-GitHat-Signature: sha256=<hmac>` so you can verify authenticity.
117
+
118
+ ## Active sessions
119
+
120
+ Users can view and revoke active sessions at `/account/sessions`. Each session
121
+ shows the user agent, IP, country, and last active time. The current session is
122
+ marked "This device". The `useSessions()` hook backs this page:
123
+
124
+ ```ts
125
+ import { useSessions } from '@githat/nextjs';
126
+ const { list, revoke, revokeAll } = useSessions();
127
+ ```
128
+
129
+ ## Sign-in activity
130
+
131
+ `/account/activity` shows an immutable audit log of every auth event
132
+ (sign-ins, MFA toggles, passkey changes, etc.) with IP, country, and device.
133
+ Paginated with `useAuditLog()`:
134
+
135
+ ```ts
136
+ import { useAuditLog } from '@githat/nextjs';
137
+ const { list } = useAuditLog();
138
+ const { events, nextCursor } = await list({ limit: 50 });
139
+ ```
140
+
103
141
  ## Two-factor authentication
104
142
 
105
143
  Users can enable TOTP-based 2FA at `/account/security` (the page is
@@ -111,9 +149,73 @@ once at enrollment for account recovery.
111
149
  `<SignInForm/>` and `<MfaChallenge/>` from `@githat/nextjs` handle
112
150
  the entire flow end-to-end — no integration code needed.
113
151
 
152
+ ## Magic-link sign-in (passwordless)
153
+
154
+ `<SignInForm/>` exposes a "Use a magic link instead" toggle. Users
155
+ type their email, get a one-time link, and sign in without a password.
156
+ The route at `/sign-in/magic` is scaffolded for you and renders
157
+ `<MagicLinkVerify/>`, which handles the token verification and
158
+ MFA-challenge handoff automatically.
159
+
160
+ Magic-link emails ship from the same per-app branded sender as
161
+ password-reset emails (verify your domain at `/account/security`
162
+ or via `useEmailDomains()` to upgrade from `auth@githat.io` to
163
+ `auth@yourdomain`).
164
+
165
+ ## Passkey sign-in (Face ID, Touch ID, hardware keys)
166
+
167
+ `<SignInForm/>` automatically shows a passkey button when the
168
+ browser supports WebAuthn. Users can register a passkey from
169
+ `/account/security` (in `<MfaManager/>`'s "Your passkeys" section).
170
+ Once registered, sign-in is one click + biometric — no email,
171
+ no password, no codes.
172
+
173
+ Passkeys are origin-bound to your domain — they only work on
174
+ your site, never elsewhere, even if a user has the same passkey
175
+ "saved" in their password manager. This is the correct security
176
+ model.
177
+
114
178
  ## Learn More
115
179
 
116
180
  - [GitHat Documentation](https://githat.io/docs)
117
181
  - [Branded Auth Emails Guide](https://githat.io/docs/email-domains)
118
182
  - [SDK Reference](https://www.npmjs.com/package/@githat/nextjs)
119
183
  - [API Reference](https://githat.io/docs/api)
184
+
185
+ ## Marketplace template
186
+
187
+ If you scaffolded with `--template marketplace`, your product catalog lives in one place:
188
+
189
+ ```
190
+ src/data/products.ts
191
+ ```
192
+
193
+ ### Adding products
194
+
195
+ Open `src/data/products.ts` and add entries to the `PRODUCTS` array:
196
+
197
+ ```ts
198
+ {
199
+ id: 'my-product', // unique, URL-safe string
200
+ name: 'My product',
201
+ description: 'A short description shown on the card.',
202
+ price: 12.99, // numeric, in your currency's base unit
203
+ category: 'food', // must match a slug in src/lib/categories.ts
204
+ inStock: true,
205
+ }
206
+ ```
207
+
208
+ The homepage, cart shell, and search page all import from this file via
209
+ `getAvailableProducts()` — no other changes needed.
210
+
211
+ ### Adding categories
212
+
213
+ Open `src/lib/categories.ts` and add entries to the `CATEGORIES` array.
214
+ Keep `slug` values lowercase and hyphen-separated (used in URLs).
215
+
216
+ ### Moving to a real database
217
+
218
+ When you're ready to persist products in a backend (Postgres, DynamoDB,
219
+ Sebastn), replace `getAvailableProducts()` in `app/page.tsx` with a
220
+ `fetch()` to your API. The `Product` interface in `src/data/products.ts`
221
+ stays the same so your UI components don't need to change.
@@ -0,0 +1,11 @@
1
+ 'use client';
2
+ import { Suspense } from 'react';
3
+ import { MagicLinkVerify } from '@githat/nextjs';
4
+
5
+ export default function MagicLinkPage() {
6
+ return (
7
+ <Suspense fallback={null}>
8
+ <MagicLinkVerify />
9
+ </Suspense>
10
+ );
11
+ }