nexu-app 2.0.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 +149 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1192 -0
- package/package.json +43 -0
- package/templates/default/.changeset/config.json +11 -0
- package/templates/default/.eslintignore +16 -0
- package/templates/default/.eslintrc.js +67 -0
- package/templates/default/.github/actions/build/action.yml +35 -0
- package/templates/default/.github/actions/quality/action.yml +53 -0
- package/templates/default/.github/dependabot.yml +51 -0
- package/templates/default/.github/workflows/deploy-dev.yml +83 -0
- package/templates/default/.github/workflows/deploy-prod.yml +83 -0
- package/templates/default/.github/workflows/deploy-rec.yml +83 -0
- package/templates/default/.husky/commit-msg +1 -0
- package/templates/default/.husky/pre-commit +1 -0
- package/templates/default/.nexu-version +1 -0
- package/templates/default/.prettierignore +7 -0
- package/templates/default/.prettierrc +19 -0
- package/templates/default/.vscode/extensions.json +14 -0
- package/templates/default/.vscode/settings.json +36 -0
- package/templates/default/apps/gitkeep +0 -0
- package/templates/default/commitlint.config.js +26 -0
- package/templates/default/docker/docker-compose.dev.yml +49 -0
- package/templates/default/docker/docker-compose.prod.yml +64 -0
- package/templates/default/docker/docker-compose.yml +6 -0
- package/templates/default/docs/architecture.md +452 -0
- package/templates/default/docs/cli.md +330 -0
- package/templates/default/docs/contributing.md +462 -0
- package/templates/default/docs/scripts.md +460 -0
- package/templates/default/gitignore +44 -0
- package/templates/default/lintstagedrc.cjs +4 -0
- package/templates/default/package.json +51 -0
- package/templates/default/packages/auth/package.json +61 -0
- package/templates/default/packages/auth/src/components/ProtectedRoute.tsx +75 -0
- package/templates/default/packages/auth/src/components/SignInForm.tsx +153 -0
- package/templates/default/packages/auth/src/components/SignUpForm.tsx +179 -0
- package/templates/default/packages/auth/src/components/SocialButtons.tsx +147 -0
- package/templates/default/packages/auth/src/components/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/index.ts +4 -0
- package/templates/default/packages/auth/src/hooks/useAuth.ts +51 -0
- package/templates/default/packages/auth/src/hooks/useRequireAuth.ts +54 -0
- package/templates/default/packages/auth/src/hooks/useSession.ts +48 -0
- package/templates/default/packages/auth/src/hooks/useUser.ts +48 -0
- package/templates/default/packages/auth/src/index.ts +45 -0
- package/templates/default/packages/auth/src/next/index.ts +18 -0
- package/templates/default/packages/auth/src/next/middleware.ts +183 -0
- package/templates/default/packages/auth/src/next/server.ts +219 -0
- package/templates/default/packages/auth/src/providers/AuthContext.tsx +435 -0
- package/templates/default/packages/auth/src/providers/index.ts +1 -0
- package/templates/default/packages/auth/src/types/index.ts +284 -0
- package/templates/default/packages/auth/src/utils/api.ts +228 -0
- package/templates/default/packages/auth/src/utils/index.ts +3 -0
- package/templates/default/packages/auth/src/utils/oauth.ts +230 -0
- package/templates/default/packages/auth/src/utils/token.ts +204 -0
- package/templates/default/packages/auth/tsconfig.json +14 -0
- package/templates/default/packages/auth/tsup.config.ts +18 -0
- package/templates/default/packages/cache/package.json +26 -0
- package/templates/default/packages/cache/src/index.ts +137 -0
- package/templates/default/packages/cache/tsconfig.json +9 -0
- package/templates/default/packages/cache/tsup.config.ts +9 -0
- package/templates/default/packages/config/eslint/index.js +20 -0
- package/templates/default/packages/config/package.json +9 -0
- package/templates/default/packages/config/typescript/base.json +26 -0
- package/templates/default/packages/constants/package.json +26 -0
- package/templates/default/packages/constants/src/index.ts +121 -0
- package/templates/default/packages/constants/tsconfig.json +9 -0
- package/templates/default/packages/constants/tsup.config.ts +9 -0
- package/templates/default/packages/logger/package.json +27 -0
- package/templates/default/packages/logger/src/index.ts +197 -0
- package/templates/default/packages/logger/tsconfig.json +11 -0
- package/templates/default/packages/logger/tsup.config.ts +9 -0
- package/templates/default/packages/result/package.json +26 -0
- package/templates/default/packages/result/src/index.ts +142 -0
- package/templates/default/packages/result/tsconfig.json +9 -0
- package/templates/default/packages/result/tsup.config.ts +9 -0
- package/templates/default/packages/types/package.json +26 -0
- package/templates/default/packages/types/src/index.ts +78 -0
- package/templates/default/packages/types/tsconfig.json +9 -0
- package/templates/default/packages/types/tsup.config.ts +10 -0
- package/templates/default/packages/ui/package.json +38 -0
- package/templates/default/packages/ui/src/components/Button.tsx +58 -0
- package/templates/default/packages/ui/src/components/Card.tsx +85 -0
- package/templates/default/packages/ui/src/components/Input.tsx +45 -0
- package/templates/default/packages/ui/src/index.ts +15 -0
- package/templates/default/packages/ui/tsconfig.json +11 -0
- package/templates/default/packages/ui/tsup.config.ts +11 -0
- package/templates/default/packages/utils/package.json +30 -0
- package/templates/default/packages/utils/src/index.test.ts +130 -0
- package/templates/default/packages/utils/src/index.ts +154 -0
- package/templates/default/packages/utils/tsconfig.json +10 -0
- package/templates/default/packages/utils/tsup.config.ts +10 -0
- package/templates/default/pnpm-workspace.yaml +3 -0
- package/templates/default/scripts/audit.mjs +700 -0
- package/templates/default/scripts/deploy.mjs +40 -0
- package/templates/default/scripts/generate-app.mjs +808 -0
- package/templates/default/scripts/lib/package-manager.mjs +186 -0
- package/templates/default/scripts/setup.mjs +102 -0
- package/templates/default/services/.env.example +16 -0
- package/templates/default/services/docker-compose.yml +207 -0
- package/templates/default/services/grafana/provisioning/dashboards/dashboards.yml +11 -0
- package/templates/default/services/grafana/provisioning/datasources/datasources.yml +9 -0
- package/templates/default/services/postgres/init/gitkeep +2 -0
- package/templates/default/services/prometheus/prometheus.yml +13 -0
- package/templates/default/tsconfig.json +27 -0
- package/templates/default/turbo.json +40 -0
- package/templates/default/vitest.config.ts +15 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, SignInFormProps } from '../types';
|
|
7
|
+
|
|
8
|
+
import { SocialButtons } from './SocialButtons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign in form component
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SignInForm
|
|
16
|
+
* providers={['google', 'github']}
|
|
17
|
+
* onSuccess={(response) => router.push('/dashboard')}
|
|
18
|
+
* showRememberMe
|
|
19
|
+
* showForgotPassword
|
|
20
|
+
* forgotPasswordUrl="/forgot-password"
|
|
21
|
+
* signUpUrl="/signup"
|
|
22
|
+
* />
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function SignInForm({
|
|
26
|
+
onSuccess,
|
|
27
|
+
onError,
|
|
28
|
+
providers = [],
|
|
29
|
+
showRememberMe = true,
|
|
30
|
+
showForgotPassword = true,
|
|
31
|
+
forgotPasswordUrl = '/forgot-password',
|
|
32
|
+
signUpUrl = '/signup',
|
|
33
|
+
redirectUrl,
|
|
34
|
+
className,
|
|
35
|
+
}: SignInFormProps) {
|
|
36
|
+
const { signIn, isLoading, error } = useAuth();
|
|
37
|
+
|
|
38
|
+
const [email, setEmail] = useState('');
|
|
39
|
+
const [password, setPassword] = useState('');
|
|
40
|
+
const [remember, setRemember] = useState(false);
|
|
41
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
42
|
+
|
|
43
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
44
|
+
e.preventDefault();
|
|
45
|
+
setFormError(null);
|
|
46
|
+
|
|
47
|
+
if (!email || !password) {
|
|
48
|
+
setFormError('Please fill in all fields');
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const response = await signIn({ email, password, remember });
|
|
54
|
+
onSuccess?.(response);
|
|
55
|
+
|
|
56
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
57
|
+
window.location.href = redirectUrl;
|
|
58
|
+
}
|
|
59
|
+
} catch (err) {
|
|
60
|
+
const authError = err as AuthError;
|
|
61
|
+
setFormError(authError.message);
|
|
62
|
+
onError?.(authError);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const displayError = formError || error?.message;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={className}>
|
|
70
|
+
<form onSubmit={e => void handleSubmit(e)}>
|
|
71
|
+
{displayError && (
|
|
72
|
+
<div role="alert" aria-live="polite">
|
|
73
|
+
{displayError}
|
|
74
|
+
</div>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<div>
|
|
78
|
+
<label htmlFor="email">Email</label>
|
|
79
|
+
<input
|
|
80
|
+
id="email"
|
|
81
|
+
name="email"
|
|
82
|
+
type="email"
|
|
83
|
+
autoComplete="email"
|
|
84
|
+
required
|
|
85
|
+
value={email}
|
|
86
|
+
onChange={e => setEmail(e.target.value)}
|
|
87
|
+
disabled={isLoading}
|
|
88
|
+
placeholder="you@example.com"
|
|
89
|
+
/>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<div>
|
|
93
|
+
<label htmlFor="password">Password</label>
|
|
94
|
+
<input
|
|
95
|
+
id="password"
|
|
96
|
+
name="password"
|
|
97
|
+
type="password"
|
|
98
|
+
autoComplete="current-password"
|
|
99
|
+
required
|
|
100
|
+
value={password}
|
|
101
|
+
onChange={e => setPassword(e.target.value)}
|
|
102
|
+
disabled={isLoading}
|
|
103
|
+
placeholder="••••••••"
|
|
104
|
+
/>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div>
|
|
108
|
+
{showRememberMe && (
|
|
109
|
+
<label>
|
|
110
|
+
<input
|
|
111
|
+
type="checkbox"
|
|
112
|
+
checked={remember}
|
|
113
|
+
onChange={e => setRemember(e.target.checked)}
|
|
114
|
+
disabled={isLoading}
|
|
115
|
+
/>
|
|
116
|
+
<span>Remember me</span>
|
|
117
|
+
</label>
|
|
118
|
+
)}
|
|
119
|
+
|
|
120
|
+
{showForgotPassword && <a href={forgotPasswordUrl}>Forgot password?</a>}
|
|
121
|
+
</div>
|
|
122
|
+
|
|
123
|
+
<button type="submit" disabled={isLoading}>
|
|
124
|
+
{isLoading ? 'Signing in...' : 'Sign in'}
|
|
125
|
+
</button>
|
|
126
|
+
</form>
|
|
127
|
+
|
|
128
|
+
{providers.length > 0 && (
|
|
129
|
+
<>
|
|
130
|
+
<div>
|
|
131
|
+
<span>Or continue with</span>
|
|
132
|
+
</div>
|
|
133
|
+
<SocialButtons
|
|
134
|
+
providers={providers}
|
|
135
|
+
mode="signin"
|
|
136
|
+
onSuccess={() => {
|
|
137
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
138
|
+
window.location.href = redirectUrl;
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
onError={onError}
|
|
142
|
+
/>
|
|
143
|
+
</>
|
|
144
|
+
)}
|
|
145
|
+
|
|
146
|
+
{signUpUrl && (
|
|
147
|
+
<p>
|
|
148
|
+
Don't have an account? <a href={signUpUrl}>Sign up</a>
|
|
149
|
+
</p>
|
|
150
|
+
)}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, SignUpFormProps } from '../types';
|
|
7
|
+
|
|
8
|
+
import { SocialButtons } from './SocialButtons';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Sign up form component
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* <SignUpForm
|
|
16
|
+
* providers={['google', 'github']}
|
|
17
|
+
* onSuccess={(response) => router.push('/onboarding')}
|
|
18
|
+
* showName
|
|
19
|
+
* signInUrl="/signin"
|
|
20
|
+
* />
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function SignUpForm({
|
|
24
|
+
onSuccess,
|
|
25
|
+
onError,
|
|
26
|
+
providers = [],
|
|
27
|
+
showName = true,
|
|
28
|
+
signInUrl = '/signin',
|
|
29
|
+
redirectUrl,
|
|
30
|
+
className,
|
|
31
|
+
}: SignUpFormProps) {
|
|
32
|
+
const { signUp, isLoading, error } = useAuth();
|
|
33
|
+
|
|
34
|
+
const [email, setEmail] = useState('');
|
|
35
|
+
const [password, setPassword] = useState('');
|
|
36
|
+
const [confirmPassword, setConfirmPassword] = useState('');
|
|
37
|
+
const [name, setName] = useState('');
|
|
38
|
+
const [formError, setFormError] = useState<string | null>(null);
|
|
39
|
+
|
|
40
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
setFormError(null);
|
|
43
|
+
|
|
44
|
+
if (!email || !password) {
|
|
45
|
+
setFormError('Please fill in all required fields');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (password !== confirmPassword) {
|
|
50
|
+
setFormError('Passwords do not match');
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (password.length < 8) {
|
|
55
|
+
setFormError('Password must be at least 8 characters');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
const response = await signUp({
|
|
61
|
+
email,
|
|
62
|
+
password,
|
|
63
|
+
name: showName ? name : undefined,
|
|
64
|
+
});
|
|
65
|
+
onSuccess?.(response);
|
|
66
|
+
|
|
67
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
68
|
+
window.location.href = redirectUrl;
|
|
69
|
+
}
|
|
70
|
+
} catch (err) {
|
|
71
|
+
const authError = err as AuthError;
|
|
72
|
+
setFormError(authError.message);
|
|
73
|
+
onError?.(authError);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const displayError = formError || error?.message;
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={className}>
|
|
81
|
+
<form onSubmit={e => void handleSubmit(e)}>
|
|
82
|
+
{displayError && (
|
|
83
|
+
<div role="alert" aria-live="polite">
|
|
84
|
+
{displayError}
|
|
85
|
+
</div>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{showName && (
|
|
89
|
+
<div>
|
|
90
|
+
<label htmlFor="name">Name</label>
|
|
91
|
+
<input
|
|
92
|
+
id="name"
|
|
93
|
+
name="name"
|
|
94
|
+
type="text"
|
|
95
|
+
autoComplete="name"
|
|
96
|
+
value={name}
|
|
97
|
+
onChange={e => setName(e.target.value)}
|
|
98
|
+
disabled={isLoading}
|
|
99
|
+
placeholder="John Doe"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<div>
|
|
105
|
+
<label htmlFor="email">Email</label>
|
|
106
|
+
<input
|
|
107
|
+
id="email"
|
|
108
|
+
name="email"
|
|
109
|
+
type="email"
|
|
110
|
+
autoComplete="email"
|
|
111
|
+
required
|
|
112
|
+
value={email}
|
|
113
|
+
onChange={e => setEmail(e.target.value)}
|
|
114
|
+
disabled={isLoading}
|
|
115
|
+
placeholder="you@example.com"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
|
|
119
|
+
<div>
|
|
120
|
+
<label htmlFor="password">Password</label>
|
|
121
|
+
<input
|
|
122
|
+
id="password"
|
|
123
|
+
name="password"
|
|
124
|
+
type="password"
|
|
125
|
+
autoComplete="new-password"
|
|
126
|
+
required
|
|
127
|
+
value={password}
|
|
128
|
+
onChange={e => setPassword(e.target.value)}
|
|
129
|
+
disabled={isLoading}
|
|
130
|
+
placeholder="••••••••"
|
|
131
|
+
/>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div>
|
|
135
|
+
<label htmlFor="confirmPassword">Confirm Password</label>
|
|
136
|
+
<input
|
|
137
|
+
id="confirmPassword"
|
|
138
|
+
name="confirmPassword"
|
|
139
|
+
type="password"
|
|
140
|
+
autoComplete="new-password"
|
|
141
|
+
required
|
|
142
|
+
value={confirmPassword}
|
|
143
|
+
onChange={e => setConfirmPassword(e.target.value)}
|
|
144
|
+
disabled={isLoading}
|
|
145
|
+
placeholder="••••••••"
|
|
146
|
+
/>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
<button type="submit" disabled={isLoading}>
|
|
150
|
+
{isLoading ? 'Creating account...' : 'Create account'}
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
|
|
154
|
+
{providers.length > 0 && (
|
|
155
|
+
<>
|
|
156
|
+
<div>
|
|
157
|
+
<span>Or continue with</span>
|
|
158
|
+
</div>
|
|
159
|
+
<SocialButtons
|
|
160
|
+
providers={providers}
|
|
161
|
+
mode="signup"
|
|
162
|
+
onSuccess={() => {
|
|
163
|
+
if (redirectUrl && typeof window !== 'undefined') {
|
|
164
|
+
window.location.href = redirectUrl;
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
onError={onError}
|
|
168
|
+
/>
|
|
169
|
+
</>
|
|
170
|
+
)}
|
|
171
|
+
|
|
172
|
+
{signInUrl && (
|
|
173
|
+
<p>
|
|
174
|
+
Already have an account? <a href={signInUrl}>Sign in</a>
|
|
175
|
+
</p>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from '../hooks/useAuth';
|
|
6
|
+
import type { AuthError, AuthProvider, SocialButtonsProps } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Social provider icons (SVG)
|
|
10
|
+
*/
|
|
11
|
+
const ProviderIcons: Record<string, React.ReactNode> = {
|
|
12
|
+
google: (
|
|
13
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
14
|
+
<path
|
|
15
|
+
d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"
|
|
16
|
+
fill="#4285F4"
|
|
17
|
+
/>
|
|
18
|
+
<path
|
|
19
|
+
d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"
|
|
20
|
+
fill="#34A853"
|
|
21
|
+
/>
|
|
22
|
+
<path
|
|
23
|
+
d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"
|
|
24
|
+
fill="#FBBC05"
|
|
25
|
+
/>
|
|
26
|
+
<path
|
|
27
|
+
d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"
|
|
28
|
+
fill="#EA4335"
|
|
29
|
+
/>
|
|
30
|
+
</svg>
|
|
31
|
+
),
|
|
32
|
+
github: (
|
|
33
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
34
|
+
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57 0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225 0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3 0 .315.225.69.825.57A12.02 12.02 0 0024 12c0-6.63-5.37-12-12-12z" />
|
|
35
|
+
</svg>
|
|
36
|
+
),
|
|
37
|
+
facebook: (
|
|
38
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="#1877F2">
|
|
39
|
+
<path d="M24 12.073c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.99 4.388 10.954 10.125 11.854v-8.385H7.078v-3.47h3.047V9.43c0-3.007 1.792-4.669 4.533-4.669 1.312 0 2.686.235 2.686.235v2.953H15.83c-1.491 0-1.956.925-1.956 1.874v2.25h3.328l-.532 3.47h-2.796v8.385C19.612 23.027 24 18.062 24 12.073z" />
|
|
40
|
+
</svg>
|
|
41
|
+
),
|
|
42
|
+
apple: (
|
|
43
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
44
|
+
<path d="M17.05 20.28c-.98.95-2.05.8-3.08.35-1.09-.46-2.09-.48-3.24 0-1.44.62-2.2.44-3.06-.35C2.79 15.25 3.51 7.59 9.05 7.31c1.35.07 2.29.74 3.08.8 1.18-.24 2.31-.93 3.57-.84 1.51.12 2.65.72 3.4 1.8-3.12 1.87-2.38 5.98.48 7.13-.57 1.5-1.31 2.99-2.54 4.09l.01-.01zM12.03 7.25c-.15-2.23 1.66-4.07 3.74-4.25.29 2.58-2.34 4.5-3.74 4.25z" />
|
|
45
|
+
</svg>
|
|
46
|
+
),
|
|
47
|
+
twitter: (
|
|
48
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="currentColor">
|
|
49
|
+
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
|
|
50
|
+
</svg>
|
|
51
|
+
),
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provider display names
|
|
56
|
+
*/
|
|
57
|
+
const ProviderNames: Record<string, string> = {
|
|
58
|
+
google: 'Google',
|
|
59
|
+
github: 'GitHub',
|
|
60
|
+
facebook: 'Facebook',
|
|
61
|
+
apple: 'Apple',
|
|
62
|
+
twitter: 'X (Twitter)',
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface SocialButtonProps {
|
|
66
|
+
provider: AuthProvider;
|
|
67
|
+
onClick: () => void;
|
|
68
|
+
disabled?: boolean;
|
|
69
|
+
mode?: 'signin' | 'signup';
|
|
70
|
+
className?: string;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Individual social login button
|
|
75
|
+
*/
|
|
76
|
+
export function SocialButton({
|
|
77
|
+
provider,
|
|
78
|
+
onClick,
|
|
79
|
+
disabled,
|
|
80
|
+
mode = 'signin',
|
|
81
|
+
className,
|
|
82
|
+
}: SocialButtonProps) {
|
|
83
|
+
const actionText = mode === 'signin' ? 'Sign in' : 'Sign up';
|
|
84
|
+
const providerName = ProviderNames[provider] || provider;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
onClick={onClick}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
className={className}
|
|
92
|
+
aria-label={`${actionText} with ${providerName}`}
|
|
93
|
+
>
|
|
94
|
+
{ProviderIcons[provider]}
|
|
95
|
+
<span>
|
|
96
|
+
{actionText} with {providerName}
|
|
97
|
+
</span>
|
|
98
|
+
</button>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Social login buttons component
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```tsx
|
|
107
|
+
* <SocialButtons
|
|
108
|
+
* providers={['google', 'github']}
|
|
109
|
+
* mode="signin"
|
|
110
|
+
* onSuccess={(provider) => console.log(`Signed in with ${provider}`)}
|
|
111
|
+
* />
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export function SocialButtons({
|
|
115
|
+
providers,
|
|
116
|
+
onSuccess,
|
|
117
|
+
onError,
|
|
118
|
+
mode = 'signin',
|
|
119
|
+
className,
|
|
120
|
+
}: SocialButtonsProps) {
|
|
121
|
+
const { signInWithProvider, isLoading } = useAuth();
|
|
122
|
+
|
|
123
|
+
const handleProviderClick = async (provider: AuthProvider) => {
|
|
124
|
+
try {
|
|
125
|
+
await signInWithProvider(provider);
|
|
126
|
+
onSuccess?.(provider);
|
|
127
|
+
} catch (error) {
|
|
128
|
+
onError?.(error as AuthError);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
if (providers.length === 0) return null;
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<div className={className}>
|
|
136
|
+
{providers.map(provider => (
|
|
137
|
+
<SocialButton
|
|
138
|
+
key={provider}
|
|
139
|
+
provider={provider}
|
|
140
|
+
onClick={() => void handleProviderClick(provider)}
|
|
141
|
+
disabled={isLoading}
|
|
142
|
+
mode={mode}
|
|
143
|
+
/>
|
|
144
|
+
))}
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAuthContext } from '../providers/AuthContext';
|
|
4
|
+
import type { UseAuthReturn } from '../types';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Hook to access authentication state and methods
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* function LoginButton() {
|
|
12
|
+
* const { isAuthenticated, user, signIn, signOut } = useAuth();
|
|
13
|
+
*
|
|
14
|
+
* if (isAuthenticated) {
|
|
15
|
+
* return (
|
|
16
|
+
* <div>
|
|
17
|
+
* <span>Welcome, {user?.name}</span>
|
|
18
|
+
* <button onClick={signOut}>Sign Out</button>
|
|
19
|
+
* </div>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
*
|
|
23
|
+
* return (
|
|
24
|
+
* <button onClick={() => signIn({ email: 'user@example.com', password: 'password' })}>
|
|
25
|
+
* Sign In
|
|
26
|
+
* </button>
|
|
27
|
+
* );
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useAuth(): UseAuthReturn {
|
|
32
|
+
const context = useAuthContext();
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
user: context.user,
|
|
36
|
+
session: context.session,
|
|
37
|
+
isAuthenticated: context.isAuthenticated,
|
|
38
|
+
isLoading: context.isLoading,
|
|
39
|
+
error: context.error,
|
|
40
|
+
signIn: context.signIn,
|
|
41
|
+
signUp: context.signUp,
|
|
42
|
+
signOut: context.signOut,
|
|
43
|
+
signInWithProvider: context.signInWithProvider,
|
|
44
|
+
refreshSession: context.refreshSession,
|
|
45
|
+
resetPassword: context.resetPassword,
|
|
46
|
+
updatePassword: context.updatePassword,
|
|
47
|
+
updateUser: context.updateUser,
|
|
48
|
+
verifyEmail: context.verifyEmail,
|
|
49
|
+
resendVerification: context.resendVerification,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuth } from './useAuth';
|
|
6
|
+
|
|
7
|
+
interface UseRequireAuthOptions {
|
|
8
|
+
/**
|
|
9
|
+
* Redirect to this URL if not authenticated
|
|
10
|
+
*/
|
|
11
|
+
redirectTo?: string;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Callback when not authenticated
|
|
15
|
+
*/
|
|
16
|
+
onUnauthenticated?: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hook to require authentication for a page/component
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* function ProtectedPage() {
|
|
25
|
+
* const { user, isLoading } = useRequireAuth({
|
|
26
|
+
* redirectTo: '/login',
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
30
|
+
*
|
|
31
|
+
* return <div>Welcome, {user?.name}!</div>;
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export function useRequireAuth(options: UseRequireAuthOptions = {}) {
|
|
36
|
+
const auth = useAuth();
|
|
37
|
+
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (auth.isLoading) return;
|
|
40
|
+
|
|
41
|
+
if (!auth.isAuthenticated) {
|
|
42
|
+
if (options.redirectTo && typeof window !== 'undefined') {
|
|
43
|
+
const currentPath = window.location.pathname + window.location.search;
|
|
44
|
+
const redirectUrl = new URL(options.redirectTo, window.location.origin);
|
|
45
|
+
redirectUrl.searchParams.set('returnTo', currentPath);
|
|
46
|
+
window.location.href = redirectUrl.toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
options.onUnauthenticated?.();
|
|
50
|
+
}
|
|
51
|
+
}, [auth.isAuthenticated, auth.isLoading, options]);
|
|
52
|
+
|
|
53
|
+
return auth;
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuthContext } from '../providers/AuthContext';
|
|
6
|
+
import type { UseSessionReturn } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to access and manage the current session
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function SessionInfo() {
|
|
14
|
+
* const { session, isLoading, isValid, getToken, refresh } = useSession();
|
|
15
|
+
*
|
|
16
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
17
|
+
* if (!session) return <div>No session</div>;
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <p>Session valid: {isValid() ? 'Yes' : 'No'}</p>
|
|
22
|
+
* <p>Expires at: {new Date(session.expiresAt).toLocaleString()}</p>
|
|
23
|
+
* <button onClick={refresh}>Refresh Session</button>
|
|
24
|
+
* </div>
|
|
25
|
+
* );
|
|
26
|
+
* }
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
export function useSession(): UseSessionReturn {
|
|
30
|
+
const context = useAuthContext();
|
|
31
|
+
|
|
32
|
+
const isValid = useCallback((): boolean => {
|
|
33
|
+
if (!context.session) return false;
|
|
34
|
+
return Date.now() < context.session.expiresAt;
|
|
35
|
+
}, [context.session]);
|
|
36
|
+
|
|
37
|
+
const getToken = useCallback((): string | null => {
|
|
38
|
+
return context.tokenManager.getAccessToken();
|
|
39
|
+
}, [context.tokenManager]);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
session: context.session,
|
|
43
|
+
isLoading: context.isLoading,
|
|
44
|
+
refresh: context.refreshSession,
|
|
45
|
+
isValid,
|
|
46
|
+
getToken,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
import { useAuthContext } from '../providers/AuthContext';
|
|
6
|
+
import type { AuthUser, UseUserReturn } from '../types';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Hook to access and manage the current user
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function UserProfile() {
|
|
14
|
+
* const { user, isLoading, update } = useUser();
|
|
15
|
+
*
|
|
16
|
+
* if (isLoading) return <div>Loading...</div>;
|
|
17
|
+
* if (!user) return <div>Not authenticated</div>;
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <div>
|
|
21
|
+
* <h1>{user.name}</h1>
|
|
22
|
+
* <button onClick={() => update({ name: 'New Name' })}>
|
|
23
|
+
* Update Name
|
|
24
|
+
* </button>
|
|
25
|
+
* </div>
|
|
26
|
+
* );
|
|
27
|
+
* }
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export function useUser(): UseUserReturn {
|
|
31
|
+
const context = useAuthContext();
|
|
32
|
+
|
|
33
|
+
const refresh = useCallback(async (): Promise<AuthUser | null> => {
|
|
34
|
+
try {
|
|
35
|
+
const user = await context.apiClient.getUser();
|
|
36
|
+
return user;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}, [context.apiClient]);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
user: context.user,
|
|
44
|
+
isLoading: context.isLoading,
|
|
45
|
+
update: context.updateUser,
|
|
46
|
+
refresh,
|
|
47
|
+
};
|
|
48
|
+
}
|